diff --git a/test_all.js b/test_all.js index bf9506ca..a74db3fd 100644 --- a/test_all.js +++ b/test_all.js @@ -4,7 +4,7 @@ const { execSync } = require('child_process') const chalk = require('chalk') const semver = require('semver') -const excludeFolder = ['node_modules'] +const excludeFolder = ['node_modules', 'website'] function getAllTests() { return fs diff --git a/website/.editorconfig b/website/.editorconfig new file mode 100644 index 00000000..a5f82dfa --- /dev/null +++ b/website/.editorconfig @@ -0,0 +1,18 @@ +# editorconfig.org + +root = true + +[*] +indent_style = space +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.py] +indent_style = space +indent_size = 4 + +[*.rb] +indent_style = space +indent_size = 4 diff --git a/website/.gitattributes b/website/.gitattributes new file mode 100644 index 00000000..53d8c853 --- /dev/null +++ b/website/.gitattributes @@ -0,0 +1,65 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# These files are text and should be normalized +*.bat text eol=crlf +*.coffee text +*.css text +*.htm text +*.html text +*.inc text +*.ini text +*.js text +*.json text +*.jsx text +*.less text +*.sass text +*.scss text +*.sh text eol=lf +*.sql text +*.ts text +*.tsx text +*.xml text +*.xhtml text + +# These files are binary and should be left untouched +# (binary is a macro for -text -diff) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.mov binary +*.mp4 binary +*.mp3 binary +*.flv binary +*.fla binary +*.swf binary +*.gz binary +*.zip binary +*.7z binary +*.ttf binary +*.eot binary +*.otf binary +*.woff binary +*.woff2 binary + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=union +*.csproj merge=union +*.vbproj merge=union +*.fsproj merge=union +*.dbproj merge=union + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 00000000..c9046a49 --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,119 @@ +# ========================= +# Node.js-Specific Ignores +# ========================= + +# Build directory +public/ + +# Gatsby cache +.cache/ + +# npm lockfile +# since we use yarn, this can be ignored +/package-lock.json + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# ========================= +# Operating System Files +# ========================= + +# OSX +# ========================= + +.DS_Store +.AppleDouble +.LSOverride + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +# Windows +# ========================= + +# Windows image file caches +Thumbs.db +ehthumbs.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk diff --git a/website/LICENSE b/website/LICENSE new file mode 100644 index 00000000..517abde0 --- /dev/null +++ b/website/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2018 Kata.ai + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE 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. diff --git a/website/README.md b/website/README.md new file mode 100644 index 00000000..33b00748 --- /dev/null +++ b/website/README.md @@ -0,0 +1,71 @@ +# The N-API Resource + +This directory contains the Markdown and supporting files that comprise the N-API Resource website published at the following URL by GitHub Pages: + +https://nodejs.github.io/node-addon-examples/ + +The website is genereated by [Gatsby](https://www.gatsbyjs.org) and published to this repository's `gh-pages` branch. + +## Objective + +The basic objective of this site is to publish additional useful information concerning the N-API technology that extends beyond the basic documentation. + +Ideally, pages published to this site should reference a working demo module that is also stored in this same repository. Features configured into the Gatsby project make embedding example source code fairly straightforward. + +## Contributing + +Submissions are gratefully accepted. Simply fork the [node-addon-examples](https://github.com/nodejs/node-addon-examples) repository containing this directory, make your changes, and submit a PR. + +All of the site's content is located in the `docs` directory. Besides the Markdown files, there is also the `toc.json` file that needs to be updated when pages are added or removed. + +### Frontmatter + +Each of the Markdown files includes front matter that Gatsby uses when formatting the site. + +| Tag | Description | +| ------- | ------------------------------------------------------------ | +| `id` | A unique identifying string for this page. | +| `title` | The title of the page as shown at the top of the page itself. | +| `prev` | The `id` of the page to be shown as the Previous link at the bottom of the page. | +| `next` | The `id` of the page to be shown as the Next link at the bottom of the page. | + +The `prev` and `next` links can be omitted for the first and last pages in the set, respectively. + +### Gatsby Basics + +This project assumes that Gatsby is installed globally. + +``` +npm install -g gatsby-cli +``` + +Be sure to set `website` as the current working directory before working on the website. For example: + +``` +cd website +``` + +These commands are useful while working on the site: + +| Command | Description | +| ---------------- | ------------------------------------------------------------ | +| `gatsby develop` | Start a hot-reloading preview site at `http://localhost:8000`. | +| `gatsby build` | Perform an optimized production build into the `public` folder. | +| `gatsby serve` | Start a local HTML server using the results from `gatsby build`. | + +### Embedding Source Code Samples + +For this project, Gatsby is configured to have the ability to copy example source code into the generated web pages. The advantage of this approach is that the web pages can be easliy regenerated whenever the source code files change. + +Here's the pattern to follow for embedding example source code: + +``` +[**package.json**](https://github.com/nodejs/node-addon-examples/blob/master/object-wrap-demo/node-addon-api/package.json) + +`embed:object-wrap-demo/node-addon-api/package.json` +``` + +> This snippet is taken from `website/docs/getting-started/objectwrap.md` + +The path in the `embed` tag is relative to the main `node-addon-examples` directory, not the `website` directory. + diff --git a/website/docs/.gitkeep b/website/docs/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/website/docs/about/uses.md b/website/docs/about/uses.md new file mode 100644 index 00000000..97fedb47 --- /dev/null +++ b/website/docs/about/uses.md @@ -0,0 +1,19 @@ +--- +id: about.uses +title: Uses for N-API +prev: about.what +--- + +## An existing C/C++ library + +Perhaps the most popular use for N-API is making the capabilities of an existing C/C++ library available to JavaScript programmers. This permits you to leverage the investment you've already made in your existing code, making it available to a whole new population of JavaScript programmers and projects. + +For many applications, the C/C++ code base is the reference implementation. N-API permits you to continue to refine and maintain your existing C/C++ code. Improvements to the C/C++ code are then easily transferred to the JavaScript users. + +## Access to OS resources + +Some applications, like those built on [Electron](https://electronjs.org) or [NW.js](https://nwjs.io), can benefit from accessing system toolkits and APIs not currently available through Node. N-API facilities accessing these system resources. + +## Computational tasks + +For low-powered devices, it may make sense to code computationally intensive tasks in C or C++ and then make that code available to JavaScript using N-API. Although in many cases Node's JavaScript runtime engine will eventually compile the JavaScript down to binary, N-API offers the ability to compile the C/C++ code just once and have the binary available to Node right from the start. \ No newline at end of file diff --git a/website/docs/about/what.md b/website/docs/about/what.md new file mode 100644 index 00000000..9baf3c75 --- /dev/null +++ b/website/docs/about/what.md @@ -0,0 +1,35 @@ +--- +id: about.what +title: What is N-API? +next: about.uses +--- + +## Node.js + +[Node.js](https://nodejs.org/en/about/) is a command line application running on development and server systems, that encapsulates a JavaScript runtime engine along with other support code primarily targeted towards implementing command line tools and network server applications. It essentially permits you to run JavaScript on the server. It is supported on nearly all current hardware and software architectures. + +## Node Modules + +Node encourages modular software development by supporting [Modules](https://nodejs.org/api/modules.html#modules_modules). Modules are essentially a colection of files and directories that conform to specific requirements. Some background information about creating Node modules can be found [here](https://docs.npmjs.com/creating-node-js-modules). + +One of the real benefits of adopting Node is the comprehensive ecosystem of Node modules available to developers. The largest collection of Node modules is available from [npm](https://www.npmjs.com). The `npm` command line tool is installed along with Node. + +## Node Native Modules + +Besides modules written in JavaScript, Node also provides technology to enable the deveopment of Node modules written primarily in C and C++. This permits existing C and C++ libraries to be compiled into Node *native* modules that are virtually indistinguishable from those written entirely in JavaScript. + +N-API is the technology that enables the development of Node native modules. + +## N-API + +[N-API](https://nodejs.org/api/n-api.html#n_api_n_api) is a toolkit introduced in Node 8.0.0 that acts as an intermediary between C/C++ code and the Node JavaScript engine. It permits C/C++ code to access, create, and manipulate JavaScript objects as if they were created by JavaScript code. N-API is built into Node versions 8.0.0 and later and requires no further installation. + +There is another Node toolkit that predates N-API, [Native Abstractions for Node.js (NAN)](https://github.com/nodejs/nan) that serves a similar purpose. NAN is implemented using direct calls to the [Chrome V8](https://developers.google.com/v8/) that Node has historically used as its JavaScript engine. The disadvantage of this approach is that the NAN layer itself, as well as code that relies on it, needs to be updated every time the V8 engine used by Node is updated. + +N-API, on the other hand, abstracts out its API from the underlying JavaScript engine. This provides two immediate benefits. The first is that N-API guarantees that the API will always be backward compatible. This means that a module you create today will continue to run on all future versions of Node without the need for running `npm install` again. Since N-API is ABI-stable, your module will continue to run even without recompilation. + +The second benefit is that your module will continue to run even if Node's underlying JavaScript engine is changed. For example, there is a lot of interest in incorporating [Microsoft's ChakraCore](https://github.com/Microsoft/ChakraCore) JavaScript engine [into Node](https://github.com/nodejs/node-chakracore). Modules built on N-API will run on the ChakraCore engine without modification. In fact, they'll run without recompilation. + +## node-addon-api + +An important adjunct to N-API, although not strictly a part of the main project, is the npm [`node-addon-api`](https://www.npmjs.com/package/node-addon-api) module. The purpose of this module is to raise the N-API API from the level of "C" up to "C++". For many users, the object model implemented by `node-addon-api` makes the effort of creating N-API modules much easier and enjoyable. \ No newline at end of file diff --git a/website/docs/build-tools/cmake-js.md b/website/docs/build-tools/cmake-js.md new file mode 100644 index 00000000..3e83d059 --- /dev/null +++ b/website/docs/build-tools/cmake-js.md @@ -0,0 +1,144 @@ +--- +id: build-tools.cmake-js +title: CMake.js +prev: build-tools.node-gyp +next: build-tools.node-pre-gyp +--- + +[CMake.js](https://github.com/cmake-js/cmake-js) is good build tool alternative to [node-gyp](node-gyp.html). CMake.js is based on the [CMake](https://cmake.org) tool which must be installed. + +### Pros + +- Uses the CMake tool which is widely-adopted in the open source community. +- Ideal for exisiting C/C++ libraries already based on CMake. + +### Cons + +- Not widely adopted in the Node community. + +## Installation + +CMake.js requires that CMake is already installed. Installers are available on the [CMake website](https://cmake.org). + +> macOS developers may find it more convenient to install CMake using [Homebrew](https://brew.sh). With Homebrew installed, CMake can be installed using a `brew install cmake` command. + +You can verify your CMake installation with the command: + +```bash +cmake --version +``` + +As a Node native module developer, you may find it convenient to install CMake.js as a global command line tool: + +```bash +npm install cmake-js -g +``` + +You can verify your CMake.js installation with the command: + +```bash +cmake-js --version +``` + +## package.json + +Your `package.json` file needs to have a couple of entires for your native module to work with CMake.js. + +Since your native module needs to be compiled using CMake.js on installation, the `scripts` propery of your `package.json` file needs an `install` entry to make this happen: + +```json + "scripts": { + "install": "cmake-js compile", + } +``` + +It is unlikely that the users of your native module will have CMake.js installed as a global command line tool. Therefore, your project needs to declare a development dependency on CMake.js. This can be accomplished by entering this command: + +```bash +npm install cmake-js --save-dev +``` + +An alternative is to manually add the development dependency to your `package.json` file: + +```json + "devDependencies": { + "cmake-js": "^6.0.0" + } +``` + +Here is a complete `package.json` file: + + [**package.json**](https://github.com/nodejs/node-addon-examples/blob/master/build_with_cmake/node-addon-api/package.json) + +`embed:build_with_cmake/node-addon-api/package.json` + +## CMakeLists.txt + +Native module built on CMake.js have a `CMakeLists.txt` that describe how the module is to be built. The file serves the same purpose as the `binding.gyp` for projects tht use `node-gyp`. + +In addition to the entries required for any CMake build, additional entries are required when building native modules. + +### CMake.js + +Here are the lines required for all native modules built using CMake.js: + +``` +project (napi-cmake-build-example) +include_directories(${CMAKE_JS_INC}) +file(GLOB SOURCE_FILES "hello.cc") +add_library(${PROJECT_NAME} SHARED ${SOURCE_FILES} ${CMAKE_JS_SRC}) +set_target_properties(${PROJECT_NAME} PROPERTIES PREFIX "" SUFFIX ".node") +target_link_libraries(${PROJECT_NAME} ${CMAKE_JS_LIB}) +``` + +### NAPI_VERSION + +When building a native module based on N-API, it is important to decalre the miniumen N-API version against which your module is designed to work. For CMake.js, this is accomplished by adding a line like this to the `CMakeLists.txt` file: + +``` +# define NAPI_VERSION +add_definitions(-DNAPI_VERSION=3) +``` + +> In the absence of other requirments, N-API version 3 is a good choice as this is the N-API version active when N-API left experimental status. + +### node-addon-api + +Additional configuration values are required for N-API modules based on `node-addon-api`. + +`node-addon-api` requires C++11. These configuration lines at the top of the `CMakeLists.txt` file specify this requirement: + +``` +cmake_minimum_required(VERSION 3.9) +cmake_policy(SET CMP0042 NEW) +set (CMAKE_CXX_STANDARD 11) +``` + +Modules based on `node-addon-api` include additional header files that are not part of Node itself. These lines instruct CMake.js where to find these files: + +``` +# Include N-API wrappers +execute_process(COMMAND node -p "require('node-addon-api').include" + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + OUTPUT_VARIABLE NODE_ADDON_API_DIR + ) +string(REPLACE "\n" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR}) +string(REPLACE "\"" "" NODE_ADDON_API_DIR ${NODE_ADDON_API_DIR}) +target_include_directories(${PROJECT_NAME} PRIVATE ${NODE_ADDON_API_DIR}) +``` + +Here is a complete `CMakeLists.txt` file for an N-API native module built on `node-addon-api`: + + [**CMakeLists.txt**](https://github.com/nodejs/node-addon-examples/blob/master/build_with_cmake/node-addon-api/CMakeLists.txt) + +`embed:build_with_cmake/node-addon-api/CMakeLists.txt` + +## Resources + +The [CMake.js](https://github.com/cmake-js/cmake-js) project home page. + +The [CMake](https://cmake.org) project home page. + +[CMake](https://cmake.org/documentation/) documentation. + +A couple of working [examples](https://github.com/nodejs/node-addon-examples/tree/master/build_with_cmake). \ No newline at end of file diff --git a/website/docs/build-tools/node-gyp.md b/website/docs/build-tools/node-gyp.md new file mode 100644 index 00000000..407dfb2e --- /dev/null +++ b/website/docs/build-tools/node-gyp.md @@ -0,0 +1,23 @@ +--- +id: build-tools.node-gyp +title: node-gyp +next: build-tools.cmake-js +--- + +Historically, [node-gyp](https://github.com/nodejs/node-gyp) has been the build tool of choice for the Node ecosystem. The tool comes bundled with Node and is nearly universally used to build Node native modules. Most of the examples on this site use node-gyp to build the binaries. + +node-gyp is based upon Google's [GYP](https://gyp.gsrc.io/) build tool. GYP, in turn, requires Python. + +> node-gyp requires Python 2.7 or Python 3.5+ depending upon the operating system on which the native module is being built. The specifics of the requirements can be found [here](https://github.com/nodejs/node-gyp#installation). + +For developers who find the node-gyp build tool too constraining, [CMake.js](cmake-js.html) is a good alternative. + +### Pros + +- Comes bundled with Node. +- Is nearly universally used by the Node community to build native modules. + +### Cons + +- The underlying GYP tool is no longer in active development by Google. +- Some developers find node-gyp too limited for their projects. diff --git a/website/docs/build-tools/node-pre-gyp.md b/website/docs/build-tools/node-pre-gyp.md new file mode 100644 index 00000000..1be89d21 --- /dev/null +++ b/website/docs/build-tools/node-pre-gyp.md @@ -0,0 +1,189 @@ +--- +id: build-tools.node-pre-gyp +title: node-pre-gyp +prev: build-tools.cmake-js +next: build-tools.prebuild +--- + +## Introduction + +One of the limitations of implementing native add-on modules is that at some point they need to be compiled and linked. In the absense of a downloadable binary, each user of a native add-on will need to compile and link the module. This requires each user to have the necessary C/C++ build tools installed. + +An alternative is for the native add-on maintainer to pre-build binaries for supported platforms and architectures and to upload these binaries to a location where users can download them. + +This is the specific solution offered by [node-pre-gyp](https://github.com/mapbox/node-pre-gyp). + +> Note that N-API support was added to node-pre-gyp in version 0.8.0. + +> [prebuild](prebuild.html) is an alternative tool that addresses this same issue. + +This page describes the modifications required to an N-API add-on module in order for it to support node-pre-gyp. + +## Amazon S3 + +By default, node-pre-gyp uploads generated binaries to the [Amazon Web Services (AWS) S3](https://aws.amazon.com/s3/) service. + +> The separate [node-pre-gyp-github](https://github.com/bchr02/node-pre-gyp-github) module implements publishing binaries to GitHub. Its use is beyond the scope of this tutorial. + + +### Amazon S3 Requirements + +Three things are required before uploading binaries to Amazon S3: + +1. An Amazon Web Services account. + +2. An AWS login that permits uploading to Amazon S3. + +3. An [Amazon S3 bucket](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html) to which the AWS login is permitted to upload objects. + +Creating these items is covered on the [Amazon Web Services](https://aws.amazon.com) pages. + +### AWS Credentials + +For security reasons, login credentials, such as those for AWS, must never be stored in repositories. For development purposes, node-pre-gyp offers a couple of different ways to store the AWS credentials outside the repository as described in more detail [here](https://github.com/mapbox/node-pre-gyp#3-configure-aws-credentials): + +1. Create a `~/.node_pre_gyprc` file with the following contents: + + ```json + { + "accessKeyId": "xxx", + "secretAccessKey": "xxx" + } + ``` + +2. Specify environment variables like this: + + ```bash + export node_pre_gyp_accessKeyId=xxx + export node_pre_gyp_secretAccessKey=xxx + ``` + +The [node-pre-gyp documentation](https://github.com/mapbox/node-pre-gyp#3-configure-aws-credentials) describes additional strategies that may be more appropriate for your CI and automated build environments. + +### More Information + +The [node-pre-gyp documentation](https://github.com/mapbox/node-pre-gyp#s3-hosting) has complete information about configuring your environments for Amazon S3. + +## package.json + +### The `dependencies` and `devDependencies` properties + +Any module using node-pre-gyp obviously has a dependency upon node-pre-gyp. In addition, `aws-sdk` is required as a devDependency since it's the code used to upload binaries to Amazon S3. + +``` +"dependencies" : { + "node-pre-gyp": "0.10.x" +}, +"devDependencies": { + "aws-sdk": "2.x" +} +``` + +### The `scripts` properties + +The `scripts` `install` property should specify `node-pre-gyp install`. The `--fallback-to-build` argument instructs node-pre-gyp to build the binary on the client machine if a suitable binary cannot be located to download. + +``` +"scripts": { + "install": "node-pre-gyp install --fallback-to-build" +} +``` + +### The `binary` property + +The `binary` property specifies which versions of N-API your native add-on supports. It also instructs node-pre-gyp where your binaries are located. + +``` +"binary": { + "module_name": "your_module", + "module_path": "./lib/binding/napi-v{napi_build_version}", + "remote_path": "./{module_name}/v{version}/{configuration}/", + "package_name": "{platform}-{arch}-napi-v{napi_build_version}.tar.gz", + "host": "https://your_bucket.s3-us-west-1.amazonaws.com", + "napi_versions": [1,3] +} +``` + +Set the `module_name` value for your project to a valid C variable name. + +The sample above shows recommended values for the `module_path`, `remote_path`, `package_name` properties. Set the appropriate bucket name and AWS region values for your project in the `host` property. + +The `napi_versions` property instructs node-pre-gyp to request one or more N-API builds. It is _required_ for N-API add-on modules. For N-API modules that do not require a specific N-API version, the recommended value is `3`. If your module requires specific N-API versions, include them in the `napi_versions` array. + +The [node-pre-gyp documentation](https://github.com/mapbox/node-pre-gyp#1-add-new-entries-to-your-packagejson) has complete information including [N-API considerations](https://github.com/mapbox/node-pre-gyp#n-api-considerations). + +## binding.gyp + +### New target + +A new target must be added to the existing `binding.gyp` file to copy the binary built by `node-gyp` into the location specified above by `module_path`. + +``` +{ + "target_name": "action_after_build", + "type": "none", + "dependencies": [ "<(module_name)" ], + "copies": [ + { + "files": [ "<(PRODUCT_DIR)/<(module_name).node" ], + "destination": "<(module_path)" + } + ] +} +``` + +### NAPI_VERSION + +The N-API header files configure themselves based on the C/C++ symbol `NAPI_VERSION` which can be communicated by node-pre-gyp to your C/C++ code by including the following property in the original target, typically the first one, in your `binding.gyp` file: + +``` +"defines": [ + "NAPI_VERSION=<(napi_build_version)", +] +``` + +## JavaScript updates + +JavaScript code that requires the native code must be updated to dynamically locate the `.node` file. + +``` +var binary = require('node-pre-gyp'); +var path = require('path'); +var binding_path = binary.find(path.resolve(path.join(__dirname,'./package.json'))); +var binding = require(binding_path); +``` + +## Build + +Once these changes are made, you can request node-pre-gyp to build your code: + +``` +npm install --build-from-source +``` + +If you have tests configured for your project, you can also run those: + +``` +npm test +``` + +## Package and publish + +The following two commands will package and publish your native add-on: + +``` +./node_modules/.bin/node-pre-gyp package +./node_modules/.bin/node-pre-gyp publish +``` + +At this point your binaries are uploaded to Amazon S3 and ready for download. + +## CI and automated builds + +The node-pre-gyp documentation describes in detail how to configure your module for [Travis CI](https://github.com/mapbox/node-pre-gyp#travis-automation) (Linux, macOS, and iOS) and [AppVeyor](https://github.com/mapbox/node-pre-gyp#appveyor-automation) (Windows). This gives you the ability to build, test, and publish binaries for system platforms and architectures that you may not otherwise have access to. + +## Resources + +[node-pre-gyp](https://github.com/mapbox/node-pre-gyp) GitHub project page + +[Amazon Web Services](https://aws.amazon.com) diff --git a/website/docs/build-tools/prebuild.md b/website/docs/build-tools/prebuild.md new file mode 100644 index 00000000..96d8388c --- /dev/null +++ b/website/docs/build-tools/prebuild.md @@ -0,0 +1,183 @@ +--- +id: build-tools.prebuild +title: prebuild +prev: build-tools.node-pre-gyp +--- + +## Introduction + +One of the limitations of implementing native add-on modules is that at some point they need to be compiled and linked. In the absence of a downloadable binary, each user of a native add-on will need to compile and link the module. This requires each user to have the necessary C/C++ build tools installed. + +An alternative is for the native add-on maintainer to pre-build binaries for supported platforms and architectures and to upload these binaries to a location where users can download them. + +This is the specific solution offered by [prebuild](https://github.com/prebuild/prebuild#prebuild). + +> Note that N-API support was added to prebuild in version 8.1.0. + +> [node-pre-gyp](node-pre-gyp.html) is an alternative tool that addresses this same issue. + +This page describes the modifications required to an N-API add-on module in order for it to support prebuild. + +## prebuild and prebuild-install + +The capabilities offered by prebuild are actually delivered by two separate packages: + +- [prebuild](https://github.com/prebuild/prebuild) — For native add-on developers. +- [prebuild-install](https://github.com/prebuild/prebuild-install) — For the users of your native add-on. + +As a developer, you will use `prebuild` to build and upload your binaries. The users of your native add-on will use `prebuild-install` to first attempt to download one of your pre-built binaries before falling back to building the binary themselves. + +## Installing + +When getting started with prebuild, it's easiest to install `prebuild` as a global package: + +```bash +npm install -g prebuild +``` + +> As you become proficient in prebuild, you may choose to alternatively install `prebuild` as one of your `devDependencies`. + +## GitHub Releases + +By default, prebuild uploads generated binaries as [GitHub Releases](https://help.github.com/en/github/administering-a-repository/about-releases). Creating and uploading GitHub releases requires the use of a GitHub personal access token. The steps to create a token are as follows: + +- Go to the Github page to create [personal access tokens](https://github.com/settings/tokens). (You may need to log in.) + +- Click `Generate new token`. + +- Under `Select scopes`, your token must have either `public_repo` or `repo` scope to upload releases. + + - Choose `public_repo` if you will be uploading only to public repositories. + + - Choose `repo` if you will be uploading to private repositories. + +- Click `Generate token`. + +- Make a note of the generated token as this will be the only time it is visible. + +- Create the `GITHUB_TOKEN` environment variable containing the token. For example: + + ``` + export GITHUB_TOKEN= + ``` + + Replace the text `` with the token generated above. + +## package.json + +### The `repository` property + +Since prebuild uploads your binary to GitHub, you must specify the GitHub repository in a `repository` property: + +```json + "repository": { + "type": "git", + "url": "https://github.com/itsmecooldude/my-napi-addon.git" + } +``` + +### The `dependencies` property + +The users of your native add-on will need to install `prebuild-install` in order to download your pre-built binary. This makes `prebuild-install` a dependency: + +```json + "dependencies": { + "prebuild-install": "^5.3.3" + } +``` + +### The `scripts` properties + +Here are some suggested `scripts` entries to get started: + +```json + "scripts": { + "install": "prebuild-install --runtime napi || node-gyp rebuild", + "rebuild": "node-gyp rebuild", + "prebuild": "prebuild --runtime napi --all --strip --verbose", + "upload": "prebuild --runtime napi --upload ${GITHUB_TOKEN}" + } +``` + +For the `prebuild` and `prebuild-install` commands, the `--runtime` argument must be `napi` to request N-API builds. When requesting N-API builds, the module's `package.json` file _must_ include a `binary` property as described next. And the `binding.gyp` `CMakeLists.txt` file _must_ include a define for `NAPI_VERSION` as described below. + +One or more `--target` arguments may be specified to request builds for specific N-API versions. N-API versions are positive integer values. Alternatively, `--all` may be used to request builds for all N-API versions supported by the module. + +In the absence of both `--target` and `--all` arguments, `prebuild` will build the most current version of the module supported by the Node instance performing the build. + +### The `binary` property + +Native modules that are designed to work with [N-API](https://nodejs.org/api/n-api.html) must explicitly declare the N-API version(s) against which they can build. This is accomplished by including a `binary` property on the module's `package.json` file. For example: + +```json +"binary": { + "napi_versions": [2,3] +} +``` + +In the absence of a need to compile against a specific N-API version, the value `3` is a good choice as this is the N-API version that was supported when N-API left experimental status. + +Modules that are built against a specific N-API version will continue to operate indefinitely, even as later versions of N-API are introduced. + +## NAPI_VERSION + +The N-API header files supplied with Node use the `NAPI_VERSION` preprocessor value supplied by the user to configure each build to the specific N-API version for which the native addon is being built. In addition, the module's C/C++ code can use this value to conditionally compile code based on the N-API version it is being compiled against. + +`prebuild` supports two build backends: [`node-gyp`](https://github.com/nodejs/node-gyp) and [`cmake-js`](https://github.com/cmake-js/cmake-js). The `NAPI_VERSION` value is configured differently for each backend. + +### node-gyp + +The following code must be included in the `binding.gyp` file of modules targeting N-API: + +```json +"defines": [ + "NAPI_VERSION=<(napi_build_version)" +] +``` + +### cmake-js + +The following line must be included in the `CMakeLists.txt` file of modules targeting N-API: + +```cmake +add_compile_definitions(NAPI_VERSION=${napi_build_version}) +``` + +## Building and uploading your binary + +Using the `package.json` `scripts` entries as a starting point, this command will build and package your native add-on: + +``` +npm run prebuild +``` + +You can run this command as often as necessary. + +When you're ready, run this command to upload your compiled binary to GitHub: + +``` +npm run upload +``` + +> This command requires that the `GITHUB_TOKEN` environment variable contains a valid GitHub token as described above. + +Once you become proficient in prebuild, you can modify the `package.json` `scripts` entries to meet your specific needs. + +## Life for your users + +You've declared `prebuild-install` as a dependency. The `install` script in your `package.json` file looks like this: + +```json + "install": "prebuild-install --runtime napi || node-gyp rebuild" +``` + +When a user installs your native add-on, the `prebuild-install` tool first looks to see if it can locate a suitable pre-built binary on GitHub for the user's specific N-API version, architecture, and platform. If the binary is found, `prebuild-install` downloads, unpacks, and installs the binaries in the correct location. + +If the binary is not found, the `install` script falls back to whatever is specified after the `||`. In this case, a `node-gyp rebuild` is triggered which will attempt to build the binary on the user's machine. + +## Resources + +[prebuild](https://github.com/prebuild/prebuild) Project GitHub page + +[prebuild-install](https://github.com/prebuild/prebuild-install) Project GitHub page + diff --git a/website/docs/getting-started/first.md b/website/docs/getting-started/first.md new file mode 100644 index 00000000..feeb1f15 --- /dev/null +++ b/website/docs/getting-started/first.md @@ -0,0 +1,181 @@ +--- +id: getting-started.first +title: A first project +prev: getting-started.tools +next: getting-started.objectwrap +--- + +Before you start, make sure you've got all the necessary [prerequisites](prerequisites.html) and [tools](tools.html) installed. + +As you select where to begin, you should be aware that N-API operates at two levels which we can think of as the "C level" and the "C++ level". + +The "C level" code is built entirely into Node itself and is very well documented on the [Node documentation pages](https://nodejs.org/api/n-api.html). If you need low-level access to the intricacies of Node, this is the tool for you. + +Alternatively, there is the [node-addon-api ](https://github.com/nodejs/node-addon-api) package which adds a C++ wrapper to the N-API code built into Node. This package makes working with N-API much easier as it implements a very nice object model and abstracts away much of the detailed coding that would otherwise be required, while retaining the N-API promise of ABI stability and forward compatibility. + +This tutorial uses `node-addon-api`. + +> N-API has been in public release and active development starting with Node 8.0.0. Since then, it's undergone a number of refinements. This tutorial has been tested with Node 10.10.0 and is known to fail with older versions of Node. You will need a copy of Node that supports N-API in order to develop and run N-API code. To see which versions of Node support N-API, refer to the [N-API Version Matrix](https://nodejs.org/api/n-api.html#n_api_n_api_version_matrix). You can determine the version of Node you're running with the command `node -v`. + +## Creating a project + +The easiest way to create a new N-API project is to use the [`generator-napi-module`](https://www.npmjs.com/package/generator-napi-module) package. As the package documentation describes, `generator-napi-module` relies on [Yeoman](http://yeoman.io) which must also be installed: + +```bash +npm install -g yo +npm install -g generator-napi-module +``` + +On some systems, you may receive the error `Error: EACCES: permission denied, access`. In that case, on Mac and Linux systems you need to run the commands with elevated privileges: + +```bash +sudo npm install -g yo +sudo npm install -g generator-napi-module +``` + +> Using [`nvm`](https://github.com/creationix/nvm) is an _excellent_ way to banish permission issues. + +Then enter these commands to generate a new project: + +```bash +mkdir hello-world +cd hello-world +yo napi-module +``` + +Here are the prompts you'll see and some suggested responses: + +``` +package name: (hello-world) +version: (1.0.0) +description: A first project. +git repository: +keywords: +author: Your name goes here +license: (ISC) +``` + +Yeoman will display the generated `package.json` file here. + +``` +Is this OK? (yes) yes +? Choose a template Hello World +? Would you like to generate TypeScript wrappers for your module? No +``` + +Yeoman will now build your "Hello World" add-on module. + +At this point, you might try running `npm test` to make sure everything is correctly installed: + +```bash +npm test +``` + +## Project structure + +At this point you have a completely functional N-API module project. The project files are structured according to N-API best practices. It should look like this: + +``` +. +├── binding.gyp Used by gyp to compile the C code +├── build The intermediary and final build products +│ └── < contents not shown here > +├── lib The N-API code that accesses the C/C++ binary +│ └── binding.js +├── node_modules Node modules required by your project +│ └── < contents not shown here > +├── package.json npm description of your module +├── package-lock.json Used by npm to insure deployment consistency +├── src The C/C++ code +│ └── hello_world.cc +└── test Test code + └── test_binding.js +``` + +Let's take a look at the essential files. + +## package.json + +[**package.json**](https://github.com/nodejs/node-addon-examples/blob/master/a-first-project/node-addon-api/package.json) + +`embed:a-first-project/node-addon-api/package.json` + +This is a typical `package.json` file as generated by [Yeoman](http://yeoman.io) from the responses we entered earlier to the `yo napi-module` command. There are a couple of entries of interest here. + +Notice the [`node-addon-api`](https://github.com/nodejs/node-addon-api) dependency. This package, which isnot strictly a part of Node, adds a C++ wrapper to the C API implemented in Node. The package makes it very straightforward to create and manipulate JavaScript objects inside C++. The package is useful even if the underlying library you're accessing is in C. + +There is also a `"gypfile": true` entry which informs npm that your package requires a build using the capabilities of the `node-gyp` package which is covered next. + +## binding.gyp + +[**binding.gyp**](https://github.com/nodejs/node-addon-examples/blob/master/a-first-project/node-addon-api/binding.gyp) + +`embed:a-first-project/node-addon-api/binding.gyp` + +One of the challenges of making C/C++ code available to Node is getting the code compiled, linked, and packaged for a variety of operating systems and architectures. Historically, this would require learning the intricacies of a variety of build tools across a number of operating systems. This is the specific issue GYP seeks to address. + +Using [GYP](https://gyp.gsrc.io/index.md) permits having a single configuration file that works across all platforms and architectures GYP supports. (It's GYP, by the way, that requires Python). + +[node-gyp](https://github.com/nodejs/node-gyp) is a command line tool built in Node that orchestrates GYP to compile your C/C++ files to the correct destination. When npm sees the `"gypfile": true` entry in your `package.json` file, it automatically invokes its own internal copy of `node-gyp` which looks for this `binding.gyp` file which must be called `binding.gyp` in order for node-gyp to locate it. + +The `binding.gyp` file is a GYP file which is thoroughly documented [here](https://gyp.gsrc.io/docs/UserDocumentation.md). There is also specific information about building libraries [here](https://gyp.gsrc.io/docs/UserDocumentation.md#skeleton-of-a-typical-library-target-in-a-gyp-file). + +## src/hello_world.cc + +[**hello_world.cc**](https://github.com/nodejs/node-addon-examples/blob/master/a-first-project/node-addon-api/src/hello_world.cc) + +`embed:a-first-project/node-addon-api/src/hello_world.cc` + +This is perhaps the simplest useful(?) N-API file you can have. + +The file defines a C++ `Method` function that takes a single `Napi::CallbackInfo&` argument. This `info` argument is used to access the JavaScript environment, including any JavaScript arguments that might be passed in. + +> `info` is an array of JavaScript arguments. + +In this case, the C++ `Method` function uses the `info` argument to create a `Napi::Env` local that is then used to create a `Napi::String` object which is returned with the value "world". + +The C++ `Init` function shows how to set a single export value for the native add-on module. In this case the name of the export is "HelloWorld" and the value is the `Method` function. + +The `NODE_API_MODULE` macro at the bottom of the C++ file insures that the `Init` function is called when the module is loaded. + +## lib/binding.js + +[**binding.js**](https://github.com/nodejs/node-addon-examples/blob/master/a-first-project/node-addon-api/lib/binding.js) + +`embed:a-first-project/node-addon-api/lib/binding.js` + +This JavaScript file defines a JavaScript class that acts as an intermediary to the C++ binary. + +In this case, the sole export of the binding is the `HelloWorld` function. + +## test/test_binding.js + +[**test_binding.js**](https://github.com/nodejs/node-addon-examples/blob/master/a-first-project/node-addon-api/test/test_binding.js) + +`embed:a-first-project/node-addon-api/test/test_binding.js` + +This code demonstrates how to load and call the `HelloWorld` function using JavaScript. Recall that the sole export from the binding is the `HelloWorld` function. The function is loaded into the `HelloWorld` variable using the `require` command. + +The `testBasic` function then calls the `HelloWorld` function and verifies the result. + +## Conclusion + +This project demonstrates a very simple N-API module that exports a single function. In addition, here are some things you might want to try: + +- Run `test_binding.js` in your debugger. See if you can step through the code to get a better understanding of how it works. What sort of visibility are you getting into the JavaScript object created by the C++ code? + +- Modify `test_binding.js` to use the C++ binary module directly instead of through `binding.js`. Step through the code in your debugger to see how things are different. + +- Modify `hello_world.cc` to access arguments passed from the JavaScript. Hint: The `node-addon-api` module comes with [examples](https://github.com/nodejs/node-addon-api#examples). + +## Resources + +- [node-addon-api](https://github.com/nodejs/node-addon-api) Documentation + +- The [generator-napi-module](https://www.npmjs.com/package/generator-napi-module) Package + +- The [node-gyp](https://www.npmjs.com/package/node-gyp) Package + +- [GYP](https://gyp.gsrc.io) and [.gyp file](https://gyp.gsrc.io/docs/UserDocumentation.md) Documentation. + +- [Yeoman](http://yeoman.io) diff --git a/website/docs/getting-started/helloworld.md b/website/docs/getting-started/helloworld.md new file mode 100644 index 00000000..bab7d3b2 --- /dev/null +++ b/website/docs/getting-started/helloworld.md @@ -0,0 +1,146 @@ +--- +id: getting-started.helloworld +title: Hello, world! +--- + +**2018-11-06** This file was created in anticipation of NodeConf EU 2018 when it was not clear whether `generator-napi-module` was going to be updated in time for the N-API workshop at the conference. It turns out that `generator-napi-module` did land in time, so this file has never been used and is not included in the `toc.json` file. + +# Hello, world! + +This tutorial will walk you through the process of creating a simple N-API module from scratch. It requires no dependencies beyond Node and npm itself. + +Before you start, make sure you've got all the necessary [prerequisites](prerequisites.html) and [tools](tools.html) installed. + +As you select where to begin, you should be aware that N-API operates at two levels which we can think of as the "C level" and the "C++ level". + +The "C level" code is built entirely into Node itself and is very well documented on the [Node documentation pages](https://nodejs.org/api/n-api.html). If you need low-level access to the intricacies of Node, this is the tool for you. + +Alternatively, there is the [node-addon-api](https://github.com/nodejs/node-addon-api) package which adds a C++ wrapper to the N-API code built into Node. This package makes working with N-API much easier as it implements a very nice object model and abstracts away much of the detailed coding that would otherwise be required, while retaining the N-API promise of ABI stability and forward compatibility. + +This tutorial uses `node-addon-api`. + +> N-API has been in public release and active development starting with Node 8.0.0. Since then, it's undergone a number of refinements. This tutorial has been tested with Node 8.9.4 and 9.5.0 and is known to fail with older versions of Node. You can determine the version of Node you're running with the command `node -v`. + +## Creating a project + +The first step is to create a directory to hold your project: + +```bash +mkdir hello-world +cd hello-world +``` + +Next run `npm init` to create a bare-bones `package.json` file in your project directory: + +```bash +npm init +``` + +Here are the prompts you'll see and some suggested responses: + +``` +package name: (hello-world) +version: (1.0.0) +description: A simple N-API example +entry point: (index.js) hello-world.js +test command: +git repository: +keywords: +author: Your name goes here +license: (ISC) +``` + +At this point your project directory should contain a simple `package.json` file populated with the information you entered above. + +## package.json + +This project uses the [node-addon-api](https://github.com/nodejs/node-addon-api) package which adds a really nice C++ wrapper to the N-API code built into Node. This dependency needs to be added to the `package.json` file: + +```json +"dependencies": { + "node-addon-api": "^1.5.0" +} +``` + +This project uses the `gtp` build system, so this needs to also be specified in the `package.json` file: + +```JSON +"gypfile": true +``` + +## binding.gyp + +One of the challenges of making C/C++ code available to Node is getting the code compiled, linked, and packaged for a variety of operating systems and architectures. Historically, this would require learning the intricacies of a variety of build tools across a number of operating systems. This is the specific issue GYP seeks to address. + +Using [GYP](https://gyp.gsrc.io/index.md) permits having a single configuration file that works across all platforms and architectures GYP supports. (It's GYP, by the way, that requires Python). + +[node-gyp](https://github.com/nodejs/node-gyp) is a command line tool built in Node that orchestrates GYP to compile your C/C++ files to the correct destination. When npm sees the `"gypfile": true` entry in your `package.json` file, it automatically invokes its own internal copy of `node-gyp` which looks for this `binding.gyp` file which must be called `binding.gyp` in order for node-gyp to locate it. + +The `binding.gyp` file is a GYP file which is thoroughly documented [here](https://gyp.gsrc.io/docs/UserDocumentation.md). There is also specific information about building libraries [here](https://gyp.gsrc.io/docs/UserDocumentation.md#skeleton-of-a-typical-library-target-in-a-gyp-file). + +Here's a `binding.gyp` file that will suffice for this project: + +{% include_code binding.gyp lang:JavaScript napi-hello-world/binding.gyp %} + +## hello-world.cc + +{% include_code src/hello-world.cc lang:cc napi-hello-world/hello-world.cc %} + +Here is the nub of our project where all the magic occurs. This is a sample C++ file that shows how to use the power of the `node-addon-api` package to access, create, and manipulate JavaScript objects in C++. + +The `napi.h` file included in the header file comes from `node-addon-api`. This is the C++ wrapper that declares a number of C++ classes representing JavaScript primitive data types and objects. + +This file declares a C++ `Greet` function that returns a `Napi::String` value. + +The C++ `Init` function declares the "exports" of the module. These are analogous to the exports declared by traditional JavaScript modules. This module exports the `Greet` function as declared above in the code. + +The macro call at the bottom of the C++ file, `NODE_API_MODULE(greet, Init)`, specifies that the `Init` function is to be called when the module is loaded. + +## hello-world.js + +{% include_code hello-world.js lang:JavaScript napi-hello-world/hello-world.js %} + +This JavaScript file shows how to load and run the code in the C++ binary. + +The module is loaded exactly like any other module using the `require` command. Once loaded, the exports of the modules can then be accessed. In this case, the sole export is the `greet` function. + +## Build and running + +Once the files are created, the code can be build and run using the following commands: + +```bash +npm install node-addon-api +npm install +node hello-world.js +``` + +The `npm install node-addon-api` command insures that the `node-addon-api` module is installed. `node-addon-api` must be installed before the next step. + +The second `npm install` builds your native module and insures all other dependencies are installed. + +The `node hello-world.js` command runs the JavaScript code that accesses the binary built in the previous step. The output should look like this: + +``` +world +``` + +## Conclusion + +This project provides a brief introduction on creating a binary module that exports a single function. In addition, here are some things you might want to try: + +- Run `hello-world.js` in your debugger. See if you can step through the code to get a better understanding of how it works. What sort of visibility are you getting into the binary module created by the C++ code? + +- Modify the C++ `Greet` function to change its behavior or add additional arguments. + +- Modify the C++ `Greet` function to return a JavaScript object instead of a string. + +- Modify `hello-world.cc` to export additional functions. + +## Resources + +- [node-addon-api](https://github.com/nodejs/node-addon-api) Documentation + +- The [node-gyp](https://www.npmjs.com/package/node-gyp) Package + +- [GYP](https://gyp.gsrc.io) and [.gyp file](https://gyp.gsrc.io/docs/UserDocumentation.md) Documentation. + diff --git a/website/docs/getting-started/migration.md b/website/docs/getting-started/migration.md new file mode 100644 index 00000000..791bcb6f --- /dev/null +++ b/website/docs/getting-started/migration.md @@ -0,0 +1,180 @@ +--- +id: getting-started.migration +title: Migration tutorial +prev: getting-started.objectwrap +--- + +The objective of this tutorial is to give you a good idea of the steps necessary and the tools available to migrate an existing [NAN](https://github.com/nodejs/nan) Node native add-on module to [N-API](https://nodejs.org/api/n-api.html) using the [node-addon-api ](https://github.com/nodejs/node-addon-api) package. + +This tutorial uses the conversion tool supplied with N-API to give you a head start on the migration. However, the conversion tool will only get you so far. Further manual cleanup is still required as described below. + +To keep things somewhat constrained, this tutorial uses [node-microtime](https://github.com/wadey/node-microtime) which is a simple NAN-based native add-on. This add-on makes system calls to determine the current time to microsecond resolution if supported by the operating system. + +Before you start, make sure you've got all the necessary [prerequisites](prerequisites.html) and [tools](tools.html) installed. + +> N-API has been in public release and active development starting with Node 8.0.0. Since then, it's undergone a number of refinements. This tutorial has been tested with Node 10.1.0 and is known to fail with older versions of Node. You can determine the version of Node you're running with the command `node -v`. + +## Clone `node-microtime` + +As a first step, clone the [node-microtime](https://github.com/wadey/node-microtime) GitHub repository to your system: + +```bash +git clone https://github.com/wadey/node-microtime.git +``` + +Before we make our modifications, it's a good idea to first build and test `node-microtime` to help verify that the necessary development tools have been correctly installed and configured. + +```bash +cd node-microtime +npm install +npm test +``` + +The `npm install` command invokes the build process and `npm test` runs the code. You may see compiler warnings that do not affect the ability to run the code. When successfully built and run, you should see output that looks something like this: + +``` +microtime.now() = 1526334357974754 +microtime.nowDouble() = 1526334357.976626 +microtime.nowStruct() = [ 1526334357, 976748 ] + +Guessing clock resolution... +Clock resolution observed: 1us +``` + +## Run the conversion tool + +Once the basic operation of the code has been verified, the next step is to run the [N-API Conversion Tool](https://github.com/nodejs/node-addon-api/blob/master/doc/conversion-tool.md). Be aware that the conversion tool _replaces files in place_. Never run the conversion tool on the only copy of your project. And obviously, you want to run it only _once_. + +``` +npm install --save node-addon-api +node ./node_modules/node-addon-api/tools/conversion.js ./ +``` + +For this small project, the conversion tool runs very quickly. At this point, the conversion tool has modified the following project files: + +- binding.gyp +- package.json +- src/microtime.cc + +Go ahead and rebuild the converted code: + +``` +npm install +``` + +As you'll see, there are one or more compile errors that need to be addressed. There can be quite a few at times, but nothing insurmountable. + +## Cleanup + +The conversion tool cannot anticipate every coding situation. So there will typically be issues that need to be addressed manually. Below are the issues you're likely to encounter with this project. The best approach is to address each issue, one at a time, and attempt the `npm install` after addressing each issue until there are no more errors. + +### Cannot find module 'nan' + +This error, and its counterpart where `napi.h` cannot be found, is due to code missing in the `bind.gyp` file. For this project, you'll see this code in the binding.gyp: + +```json +'include_dirs' : [ ' N-API has been in public release and active development starting with Node 8.0.0. Since then, it's undergone a number of refinements. This tutorial has been tested with Node 10.10.0 and is known to fail with older versions of Node. You can determine the version of Node you're running with the command `node -v`. + +## Creating a project + +The easiest way to create a new N-API project is to use the [`generator-napi-module`](https://www.npmjs.com/package/generator-napi-module) package. As the package documentation describes, `generator-napi-module` relies on [Yeoman](http://yeoman.io) which must also be installed: + +```bash +npm install -g yo +npm install -g generator-napi-module +``` + +On some systems, you may receive the error `Error: EACCES: permission denied, access`. In that case, on Mac and Linux systems you need to run the commands with elevated privileges: + +```bash +sudo npm install -g yo +sudo npm install -g generator-napi-module +``` + +> Using [`nvm`](https://github.com/creationix/nvm) is an _excellent_ way to banish permission issues. + +Then enter these commands to generate a new project: + +```bash +mkdir object-wrap-demo +cd object-wrap-demo +yo napi-module +``` + +Here are the prompts you'll see and some suggested responses: + +``` +package name: (object-wrap-demo) +version: (1.0.0) +description: An object wrapper demo +git repository: +keywords: +author: Your name goes here +license: (ISC) +``` + +Yeoman will display the generated `package.json` file here. + +``` +Is this OK? (yes) yes +? Choose a template Object Wrap +? Would you like to generate TypeScript wrappers for your module? No +``` + +Yeoman will now build your "Hello World" add-on module. + +At this point, you might try running `npm test` to make sure everything is correctly installed: + +```bash +npm test +``` + +## Project structure + +At this point you have a completely functional N-API module project. The project files are structured according to N-API best practices. It should look like this: + +``` +. +├── binding.gyp Used by gyp to compile the C code +├── build The intermdiary and final build products +│   └── < contents not shown here > +├── lib The N-API code that accesses the C/C++ binary +│   └── binding.js +├── node_modules Node modules required by your project +│   └── < contents not shown here > +├── package.json npm description of your module +├── package-lock.json Used by npm to insure deployment consistency +├── src The C/C++ code +│   ├── object_wrap_demo.cc +│   └── object_wrap_demo.h +└── test Test code + └── test_binding.js +``` + +Let's take a look at the essential files. + +## package.json + +[**package.json**](https://github.com/nodejs/node-addon-examples/blob/master/object-wrap-demo/node-addon-api/package.json) + +`embed:object-wrap-demo/node-addon-api/package.json` + +This is a typical `package.json` file as generated by [Yeoman](http://yeoman.io) from the responses we entered earlier to the `yo napi-module` command. There are a couple of entries of interest here. + +Notice the [`node-addon-api`](https://github.com/nodejs/node-addon-api) dependency. This package, which is not strictly a part of Node, adds a C++ wrapper to the C API implemented in Node. The package makes it very straightforward to create and manipulate JavaScript objects inside C++. The package is useful even if the underlying library you're accessing is in C. + +There is also a `"gypfile": true` entry which informs npm that your package requires a build using the capabilities of the `node-gyp` package which is covered next. + +## binding.gyp + +[**binding.gyp**](https://github.com/nodejs/node-addon-examples/blob/master/object-wrap-demo/node-addon-api/binding.gyp) + +`embed:object-wrap-demo/node-addon-api/binding.gyp` + +One of the challenges of making C/C++ code available to Node is getting the code compiled, linked, and packaged for a variety of operating systems and architectures. Historically, this would require learning the intricacies of a variety of build tools across a number of operating systems. This is the specific issue GYP seeks to address. + +Using [GYP](https://gyp.gsrc.io/index.md) permits having a single configuration file that works across all platforms and architectures GYP supports. (It's GYP, by the way, that requires Python). + +[node-gyp](https://github.com/nodejs/node-gyp) is a command line tool built in Node that orchestrates GYP to compile your C/C++ files to the correct destination. When npm sees the `"gypfile": true` entry in your `package.json` file, it automatically invokes its own internal copy of `node-gyp` which looks for this `binding.gyp` file which must be called `binding.gyp` in order for node-gyp to locate it. + +The `binding.gyp` file is a GYP file which is thoroughly documented [here](https://gyp.gsrc.io/docs/UserDocumentation.md). There is also specific information about building libraries [here](https://gyp.gsrc.io/docs/UserDocumentation.md#skeleton-of-a-typical-library-target-in-a-gyp-file). + +## src/object\_wrap\_demo.h and src/object\_wrap\_demo.cc + +[**object\_wrap\_demo.h**](https://github.com/nodejs/node-addon-examples/blob/master/object-wrap-demo/node-addon-api/src/object_wrap_demo.h) + +`embed:object-wrap-demo/node-addon-api/src/object_wrap_demo.h` + +[**object\_wrap\_demo.cc**](https://github.com/nodejs/node-addon-examples/blob/master/object-wrap-demo/node-addon-api/src/object_wrap_demo.cc) + +`embed:object-wrap-demo/node-addon-api/src/object_wrap_demo.cc` + +Here is the nub of our project where all the magic occurs. This is a sample C++ file that shows how to use the power of the `node-addon-api` package to access, create, and manipulate JavaScript objects in C++. + +The `napi.h` file included in the header file comes from `node-addon-api`. This is the C++ wrapper that declares a number of C++ classes representing JavaScript primitive data types and objects. + +The `object_wrap_demo.cc` file defines a C++ object called `ObjectWrapDemo` with a constructor that takes a single JavaScript string as the argument. The constructor stores this string in its private data member `_greeterName`. + +The code also defines a `ObjectWrapDemo::Greet` method that takes a single JavaScript string as the argument. The method prints two strings to stdout and returns a JavaScript string containing the value originally passed to the constructor. + +The `ObjectWrapDemo::GetClass` static method returns a class definition that N-API uses in order know how to call the methods implemented by the C++ class. + +The `Init` function declares the "exports" of the module. These are analogous to the exports declared by traditional JavaScript modules. This module exports the `ObjectWrapDemo` class as declared by the `ObjectWrapDemo::GetClass` static method. + +The macro call at the bottom of the C++ file, `NODE_API_MODULE(addon, Init)`, specifies that the `Init` function is to be called when the module is loaded. + +## lib/binding.js + +[**binding.js**](https://github.com/nodejs/node-addon-examples/blob/master/object-wrap-demo/node-addon-api/lib/binding.js) + +`embed:object-wrap-demo/node-addon-api/lib/binding.js` + +This JavaScript file defines a JavaScript class that acts as an intermediary to the C++ binary. + +The file defines a `ObjectWrapDemo` JavaScript class and then exports it. When `new ObjectWrapDemo (value)` is invoked, the JavaScript class creates a `ObjectWrapDemo` object using the N-API binary and stores it internally as `_addonInstance`. The `_addonInstance` value is used by the JavaScript `greet` method to call the same method in the C++ binary. + +## test/test_binding.js + +[**test_binding.js**](https://github.com/nodejs/node-addon-examples/blob/master/object-wrap-demo/node-addon-api/test/test_binding.js) + +`embed:object-wrap-demo/node-addon-api/test/test_binding.js` + +This code demonstrates how to use the `ObjectWrapDemo` JavaScript class defined in `lib/binding.js`. + +Note that as a side-effect of the `printf` code in the C++ module, two text strings are written to stdout each time the `greet` method is called. + +## Conclusion + +This project provides a solid foundation on which you can create your own N-API modules. In addition, here are some things you might want to try: + +- Run `test_binding.js` in your debugger. See if you can step through the code to get a better understanding of how it works. What sort of visibility are you getting into the JavaScript object created by the C++ code? + +- Modify `test_binding.js` to use the C++ binary module directly instead of through `binding.js`. Step through the code in your debugger to see how things are different. + +- Modify `object_wrap_demo.cc`, or create a new C++ module, to export functions instead of an object. + +## Resources + +- [node-addon-api](https://github.com/nodejs/node-addon-api) Documentation + +- The [generator-napi-module](https://www.npmjs.com/package/generator-napi-module) Package + +- The [node-gyp](https://www.npmjs.com/package/node-gyp) Package + +- [GYP](https://gyp.gsrc.io) and [.gyp file](https://gyp.gsrc.io/docs/UserDocumentation.md) Documentation. + +- [Yeoman](http://yeoman.io) diff --git a/website/docs/getting-started/prerequisites.md b/website/docs/getting-started/prerequisites.md new file mode 100644 index 00000000..5eba681a --- /dev/null +++ b/website/docs/getting-started/prerequisites.md @@ -0,0 +1,15 @@ +--- +id: getting-started.prerequisites +title: Prerequisites +next: getting-started.tools +--- + +## C/C++ and JavaScript + +The purpose of N-API is to enable you to use your existing or newly written C/C++ code from JavaScript. This assumes at least a passing familiarity with both JavaScript and C/C++. The level of familiarity necessary is dependent upon the complexity of your project and your involvement with the JavaScript and the C/C++. + +For your own projects, it will be necessary to have an understadning of the C/C++ code you plan to integrate as well as what objectives you hope to achieve with your project on the JavaScript side of things. + +## Command line tools + +Many of the tools used to develop N-API modules are run from the command line. So at least a passing familiarity and confidence with your command line tool is essential. IDEs, as described [here](tools.html#Other-tools) can help. But they also rely in the command line. \ No newline at end of file diff --git a/website/docs/getting-started/tools.md b/website/docs/getting-started/tools.md new file mode 100644 index 00000000..3dd0f144 --- /dev/null +++ b/website/docs/getting-started/tools.md @@ -0,0 +1,100 @@ +--- +id: getting-started.tools +title: The tools you’ll need +prev: getting-started.prerequisites +next: getting-started.first +--- + +## Node + +[Node](https://nodejs.org/en/about/) is the software that runs JavaScript on your computer. It bundles the [Chrome V8 JavaScript engine](https://developers.google.com/v8/) with a set of other tools that permit you to run your JavaScript code outside of the browser. + +The easiest way to install Node on your system is to download the appropriate installer from [here](https://nodejs.org/en/). The LTS (Long Term Support) version is the most stable version and is most likely the one you want. The Current version is the latest available and includes experimental features. The Windows, macOS, and Linux installers also include a relatively recent copy of npm, which is covered next. + +> Note that N-API was first supported experimentally in Node 8.0.0. You will need a copy of Node that supports N-API in order to develop and run N-API code. To see which versions of Node support N-API, refer to the [N-API Version Matrix](https://nodejs.org/api/n-api.html#n_api_n_api_version_matrix). + +## npm + +[npm](https://www.npmjs.com), the "Node Package Manager", is a set of tools for creating, maintaining, and sharing JavaScript modules. A module is a single set of JavaScript and other files that perform a specific useful purpose. You'll be using modules created by others as well as creating your own modules. npm facilities a vibrant open source community and offers [accounts](https://www.npmjs.com/pricing) that permit individuals and organizations to publish private modules. + +For most N-API users, the objective is to publish an npm module containing their C/C++ code and the N-API code + +npm is typically installed alongside Node. There is more information [here](https://www.npmjs.com/get-npm) about installing npm and keeping it up-to-date. + +## Git + +Although not strictly required for N-API work, [Git](https://git-scm.com) is a widely used distributed version control system used in many software development projects. Git is beyond the scope of this site. + +## C/C++ and Python + +Besides Node and npm, you'll need a set of C and C++ development tools and Python. + +### Windows + +It is not necessary to install the entire Visual Studio IDE in order to develop and run N-API modules on Windows. There is an npm module, [`windows-build-tools`](https://www.npmjs.com/package/windows-build-tools), that has everything you need. + +Open [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell?view=powershell-6) or `cmd.exe` as *Administrator* and enter: + +```bash +npm install --global --production windows-build-tools +``` + +This module also installs Python. + +### Mac + +Assuming you haven't yet installed Apple's Xcode IDE, the most direct route for installing the C/C++ tools you need for N-API is to run this command in the Terminal program: + +```bash +xcode-select --install +``` + +If this fails for some reason, you will need to install the complete [Xcode IDE](https://developer.apple.com/xcode/ide/) which will also installs the necessary C/C++ command line tools. + +Python comes installed on all versions of macOS prior to macOS Catalina. This command will verify if Python is installed on your system: + +```bash +python --version +``` + +If needed, installers for Python can be found [here](https://www.python.org/downloads/). + +### Linux + +The necessary C/C++ toolchain and Python are typically already installed on most current Linux distributions. If they are missing on your system, the best way to install the tools is determined by the particular Linux distribution you are using. + +Information about installing the LLVM C/C++ toolchain can be found [here](https://llvm.org). Installers for Python can be found [here](https://www.python.org/downloads/). + +## Verifying your tools + +After installing all of the above tools, each of the commands shown below should return useful information. + +### Mac and Linux + +```bash +node --version +npm --version +python --version +git --version +cc --version +make --version +``` + +### Windows + +```bash +node --version +npm --version +python --version (TBD) +git --version +cc --version (TBD) +make --version (TBD) +``` + +## Other tools + +You'll need your favorite shell program which typically comes pre-installed on macOS and Linux. For Windows, you may want to consider [PowerShell](https://docs.microsoft.com/en-us/powershell/scripting/getting-started/getting-started-with-windows-powershell?view=powershell-6). + +And you'll need your favorite code editor. + +There are also powerful IDEs (Integrated Development Environments) that are popular in the developer community. Among these are [Visual Studio Code](https://code.visualstudio.com) and [WebStorm](https://www.jetbrains.com/webstorm/). Depending upon your level of involvement with this work, these tools can quickly pay for themselves in increased productivity. \ No newline at end of file diff --git a/website/docs/index.md b/website/docs/index.md new file mode 100644 index 00000000..aac114e3 --- /dev/null +++ b/website/docs/index.md @@ -0,0 +1,21 @@ +--- +layout: home +id: index +title: Welcome to the N-API Resource +--- + +The goal of this site is to be a clearinghouse for everything related [Node's](https://nodejs.org/en/about/) ABI-Stable C/C++ API, [N-API](https://nodejs.org/api/n-api.html#n_api_n_api). + +If you are looking to make existing C/C++ code accessible to the widening universe of JavaScript projects, or if you have need to access operating system's resources from JavaScript, or if you have a particularly computationally intensive task that would benefit from hand-tuned C/C++ code accessible from JavaScript, N-API may be a good fit for you. + +This site is maintained by members of the Node.js N-API team. Please let us know if there is anything we can do to answer your questions or make this site better. We welcome your feedback through the link available at the bottom of each page. + +## Contributors + +| Name | GitHub Link | +| ---- | ----------- | +| Gabriel Schulhof | [gabrielschulhof](https://github.com/gabrielschulhof) | +| Nicola Del Gobbo | [NickNaso](https://github.com/NickNaso) | +| Jim Schlight | [jschlight](https://github.com/jschlight) | +| Michael Dawson | [mhdawson](https://github.com/mhdawson) | +| Kevin Eady | [KevinEady](https://github.com/KevinEady) | diff --git a/website/docs/menu.json b/website/docs/menu.json new file mode 100644 index 00000000..c97495c7 --- /dev/null +++ b/website/docs/menu.json @@ -0,0 +1,26 @@ +[ + { + "id": "getting-started", + "label": "About N-API", + "href": "/about/what", + "external": false + }, + { + "id": "n-api", + "label": "N-API Documentation", + "href": "https://nodejs.org/api/n-api.html#n_api_n_api", + "external": true + }, + { + "id": "node-addon-api", + "label": "node-addon-api Module", + "href": "https://github.com/nodejs/node-addon-api", + "external": true + }, + { + "id": "examples", + "label": "Examples", + "href": "https://github.com/nodejs/node-addon-examples", + "external": true + } +] diff --git a/website/docs/special-topics/asyncworker.md b/website/docs/special-topics/asyncworker.md new file mode 100644 index 00000000..a6234560 --- /dev/null +++ b/website/docs/special-topics/asyncworker.md @@ -0,0 +1,99 @@ +--- +id: special-topics.asyncworker +title: AsyncWorker +prev: special-topics.object-function-refs +next: special-topics.thread-safe-functions +--- + +## Introduction + +You may have a project in which you have a piece of long-running C/C++ code that you want run in the background instead of on Node's main event loop. N-API's [`AsyncWorker`](https://github.com/nodejs/node-addon-api/blob/master/doc/async_worker.md) class is designed specifically for this case. + +As a programmer, your job is essentially to subclass `AsyncWorker` and to implement the `Execute` method. You'll also probably implement a wrapper function to make using your `AsyncWorker` easier. + +In this example, we're going to create a `SimpleAsyncWorker` class that subclasses `AsyncWorker`. The worker will take an integer value indicating the length of time it is to "work." When the worker completes, it will return a text string indicating how long it worked. In one case, the worker will indicate an error instead. + +## SimpleAsyncWorker + +Here's the C++ header file for `SimpleAsyncWorker`: + +[**SimpleAsyncWorker.h**](https://github.com/nodejs/node-addon-examples/blob/master/napi-asyncworker-example/node-addon-api/src/SimpleAsyncWorker.h) + +`embed:napi-asyncworker-example/node-addon-api/src/SimpleAsyncWorker.h` + +This code declares a constructor that takes as an argument the length of time (in seconds) the `SimpleAsyncWorker` is to run. A private data member is declared to hold this value. + +The header also declares two methods, `Execute` and `OnOK`, which override methods declared by `AsyncWorker`, and are described in more detail below. + +And here's the C++ implementation file for `SimpleAsyncWorker`: + +[**SimpleAsyncWorker.cc**](https://github.com/nodejs/node-addon-examples/blob/master/napi-asyncworker-example/node-addon-api/src/SimpleAsyncWorker.cc) + +`embed:napi-asyncworker-example/node-addon-api/src/SimpleAsyncWorker.cc` + +The constructor takes two arguments. `callback` is the JavaScript function that gets called when the `Execute` method returns. `callback` gets called whether there was an error or not. The second constructor argument, `runTime`, is an integer value indicating how long (in seconds) the worker is to run. + +Node will run the code of the `Execute` method in a thread separate from the thread running Node's main event loop. The `Execute` method _has no access_ to any part of the N-API environment. For this reason, the `runTime` input value was stored by the constructor as a private data member. + +In this implementation, the `Execute` method simply waits the number of seconds specified earlier by `runTime`. This is where the long running code goes in a real implementation. To demonstrate how error handling works, this `Execute` method declares an error when requested to run 4 seconds. + +The `OnOK` method is called after the `Execute` method returns unless the `Execute` method calls `SetError` or in the case where C++ exceptions are enabled and an exception is thrown. In the case of an error, the `OnError` method is called instead of `OnOK`. The default `OnError` implementation simply calls the `AsyncWorker` callback function with the error as the only argument. + +In this implementation, the `OnOK` method formulates a string value and passes it as the _second_ argument to the `callback` function specified in the constructor. The first argument passed to the `callback` function is a JavaScript `null` value. The reason for this is that a single callback function is called whether an error occurs or not. The default `OnError` method, which `SimpleAsyncWorker` does not override, passes the error as the first argument to the callback. This will become more clear in the next section. + +Note that unlike `Execute`, the `OnOK` and `OnError` methods _do_ have access to the N-API environment. + +## RunSimpleAsyncWorker + +We need a C++ function that instantiates `SimpleAsyncWorker` objects and requests them to be queued. This function needs to be registered with N-API so that it is accessible from the JavaScript code. + +[**RunSimpleAsyncWorker.cc**](https://github.com/nodejs/node-addon-examples/blob/master/napi-asyncworker-example/node-addon-api/src/RunSimpleAsyncWorker.cc) + +`embed:napi-asyncworker-example/node-addon-api/src/RunSimpleAsyncWorker.cc` + +The `runSimpleAsyncWorker` function, which is accessible from JavaScript, takes two arguments which are passed through the `info` argument. The first argument, which is passed as `info[0]`, is the `runTime` and the second argument is the JavaScript callback function which gets called when the `Execute` method returns. + +The code then instantiates a `SimpleAsyncWorker` object and requests that it be queued for possible execution on the next tick. Unless the `SimpleAsyncWorker` object is queued, its `Execute` method will never be called. + +Once the `SimpleAsyncWorker` object is queued, `runSimpleAsyncWorker` formulates a text string and returns it to the caller. + +## Running in JavaScript + +Here's a simple JavaScript program that shows how to run `SimpleAsyncWorker` instances. + +[**Test.js**](https://github.com/nodejs/node-addon-examples/blob/master/napi-asyncworker-example/node-addon-api/test/Test.js) + +`embed:napi-asyncworker-example/node-addon-api/test/Test.js` + +In this code, the `runSimpleAsyncWorker` function is called three times, each with a different `runTime` parameter. Each call specifies `AsyncWorkerCompletion` as the callback function. + +The `AsyncWorkerCompletion` function is coded to handle the cases where the `Execute` method reports an error and when it does not. It simply logs to the console when it's called. + +Here's what the output looks like when the JavaScript successfully runs: + +``` +runSimpleAsyncWorker returned 'SimpleAsyncWorker for 2 seconds queued.'. +runSimpleAsyncWorker returned 'SimpleAsyncWorker for 4 seconds queued.'. +runSimpleAsyncWorker returned 'SimpleAsyncWorker for 8 seconds queued.'. +SimpleAsyncWorker returned 'SimpleAsyncWorker returning after 'working' 2 seconds.'. +SimpleAsyncWorker returned an error: [Error: Oops! Failed after 'working' 4 seconds.] +SimpleAsyncWorker returned 'SimpleAsyncWorker returning after 'working' 8 seconds.'. +``` + +As expected, each call to `runSimpleAsyncWorker` immediately returns. The `AsyncWorkerCompletion` function gets called when each `SimpleAsyncWorker` completes. + +## Caveats + +- It is _absolutely essential_ that the `Execute` method makes no N-API calls. This means that the `Execute` method has _no access_ to any input values passed by the JavaScript code. + + Typically, the `AsyncWorker` class constructor will collect the information it needs from the JavaScript objects and store _copies_ of that information as data members. The results of the `Execute` method can then be turned back into JavaScript objects in the `OnOK` method. + +- The Node process is aware of all running `Execute` methods and will not terminate until all running `Execute` methods have returned. + +- An AsyncWorker can be safely terminated with a call to `AsyncWorker::Cancel` from the main thread. + +## Resources + +[AsyncWorker Class Documentation](https://github.com/nodejs/node-addon-api/blob/master/doc/async_worker.md). + +The complete source and build files for this project can be found at [inspiredware/napi-asyncworker-example](https://github.com/inspiredware/napi-asyncworker-example). diff --git a/website/docs/special-topics/context-awareness.md b/website/docs/special-topics/context-awareness.md new file mode 100644 index 00000000..dcbd34db --- /dev/null +++ b/website/docs/special-topics/context-awareness.md @@ -0,0 +1,271 @@ +--- +id: special-topics.context-awareness +title: Context awareness +prev: special-topics.thread-safe-functions +--- + +## Background + +Node.js has historically run as a single-threaded process. This all changed with the introduction of [Worker Threads](https://nodejs.org/api/worker_threads.html#worker_threads_worker_threads) in Node 10. Worker Threads add a JavaScript-friendly concurrency abstraction that native add-on developers need to be aware of. What this means practically is that your native add-on may be loaded and unloaded more than once and its code may be executed concurrently in multiple threads. There are specific steps you must take to insure your native add-on code runs correctly. + +The Worker Thread model specifies that each Worker runs completely independently of each other and communicate to the parent Worker using a MessagePort object supplied by the parent. This makes the Worker Threads essentially isolated from one another. The same is true for your native add-on. + +Each Worker Thread operates within its own environment which is also referred to as a context. The context is available to each N-API function as an [`napi_env`](https://nodejs.org/api/n-api.html#n_api_napi_env) value. + +## Multiple loading and unloading + +If your native add-on requires persistent memory, allocating this memory in static global space is a recipe for disaster. Instead, it is *essential* that this memory is allocated each time within the context in which the native add-on is initialized. This memory is typically allocated in your native add-on's `Init` method. But in some cases it can also be allocated as your native add-on is running. + +In addition to the multiple loading described above, your native add-on is also subject to automatic unloading by the JavaScript runtime engine's garbage collector when your native add-on is no longer in use. To prevent memory leaks, any memory your native add-on has allocated *must* be freed when you native add-on is unloaded. + +The next sections describe two different techniques you can use to allocate and free persistent memory associated with your native add-on. The techniques may be used individually or together in your native add-on. + +## Instance data + +> Note that the feature described here is currently experimental in Node 12.8.0 and later. + +N-API gives you the ability to associate a single piece of memory your native-add allocates with the context under which it is running. This technique is called "instance data" and is useful when your native add-on allocates a single piece of data when its loaded. + +The `napi_set_instance_data` allows your native add-on to associate a single piece of allocated memory with the context under which you native add-on is loaded. The `napi_get_instance_data` can then be called anywhere in you native add-on to retrieve the location of the memory that was allocated. + +You specify a finalizer callback in your `napi_set_instance_data` call. The finalizer callback gets called when your native add-on is released from memory and is where you should release the memory associated with this context. + +### Resources + +[Environment Life Cycle APIs](https://nodejs.org/api/n-api.html#n_api_environment_life_cycle_apis) Node.js documentation for `napi_set_instance_data` and `napi_get_instance_data`. + +### Example + +In this example, a number of Worker Threads are created. Each Worker Thread creates an `AddonData` struct that is tied to the Worker Thread's context using a call to `napi_set_instance_data`. Over time, the value held in the struct is incremented and decremented using a computationally expensive operation. + +In time, the Worker Threads complete their operations at which time the allocated struct is freed in the `DeleteAddonData` function. + +#### binding.c + +```c +#include +#include +#include + +#define NAPI_EXPERIMENTAL +#include + +// Structure containing information needed for as long as the addon exists. It +// replaces the use of global static data with per-addon-instance data by +// associating an instance of this structure with each instance of this addon +// during addon initialization. The instance of this structure is then passed to +// each binding the addon provides. Thus, the data stored in an instance of this +// structure is available to each binding, just as global static data would be. +typedef struct { + double value; +} AddonData; + +// This is the actual, useful work performed: increment or decrement the value +// stored per addon instance after passing it through a CPU-consuming but +// otherwise useless calculation. +static int ModifyAddonData(AddonData* data, double offset) { + // Expensively increment or decrement the value. + data->value = tan(atan(exp(log(sqrt(data->value * data->value))))) + offset; + + // Round the value to the nearest integer. + data->value = + (double)(((int)data->value) + + (data->value - ((double)(int)data->value) > 0.5 ? 1 : 0)); + + // Return the value as an integer. + return (int)(data->value); +} + +// This is boilerplate. The instance of the `AddonData` structure created during +// addon initialization must be destroyed when the addon is unloaded. This +// function will be called when the addon's `exports` object is garbage collected. +static void DeleteAddonData(napi_env env, void* data, void* hint) { + // Avoid unused parameter warnings. + (void) env; + (void) hint; + + // Free the per-addon-instance data. + free(data); +} + +// This is also boilerplate. It creates and initializes an instance of the +// `AddonData` structure and ties its lifecycle to that of the addon instance's +// `exports` object. This means that the data will be available to this instance +// of the addon for as long as the JavaScript engine keeps it alive. +static AddonData* CreateAddonData(napi_env env, napi_value exports) { + AddonData* result = malloc(sizeof(*result)); + result->value = 0.0; + assert(napi_set_instance_data(env, result, DeleteAddonData, NULL) == napi_ok); + return result; +} + +// This function is called from JavaScript. It uses an expensive operation to +// increment the value stored inside the `AddonData` structure by one. +static napi_value Increment(napi_env env, napi_callback_info info) { + // Retrieve the per-addon-instance data. + AddonData* addon_data = NULL; + assert(napi_get_instance_data(env, ((void**)&addon_data)) == napi_ok); + + // Increment the per-addon-instance value and create a new JavaScript integer + // from it. + napi_value result; + assert(napi_create_int32(env, + ModifyAddonData(addon_data, 1.0), + &result) == napi_ok); + + // Return the JavaScript integer back to JavaScript. + return result; +} + +// This function is called from JavaScript. It uses an expensive operation to +// decrement the value stored inside the `AddonData` structure by one. +static napi_value Decrement(napi_env env, napi_callback_info info) { + // Retrieve the per-addon-instance data. + AddonData* addon_data = NULL; + assert(napi_get_instance_data(env, ((void**)&addon_data)) == napi_ok); + + // Decrement the per-addon-instance value and create a new JavaScript integer + // from it. + napi_value result; + assert(napi_create_int32(env, + ModifyAddonData(addon_data, -1.0), + &result) == napi_ok); + + // Return the JavaScript integer back to JavaScript. + return result; +} + +// Initialize the addon in such a way that it may be initialized multiple times +// per process. The function body following this macro is provided the value +// `env` which has type `napi_env` and the value `exports` which has type +// `napi_value` and which refers to a JavaScript object that ultimately contains +// the functions this addon wishes to expose. At the end, it must return a +// `napi_value`. It may return `exports`, or it may create a new `napi_value` +// and return that instead. +NAPI_MODULE_INIT(/*env, exports*/) { + // Create a new instance of the per-instance-data that will be associated with + // the instance of the addon being initialized here and that will be destroyed + // along with the instance of the addon. + AddonData* addon_data = CreateAddonData(env, exports); + + // Declare the bindings this addon provides. The data created above is given + // as the last initializer parameter, and will be given to the binding when it + // is called. + napi_property_descriptor bindings[] = { + {"increment", NULL, Increment, NULL, NULL, NULL, napi_enumerable, addon_data}, + {"decrement", NULL, Decrement, NULL, NULL, NULL, napi_enumerable, addon_data} + }; + + // Expose the two bindings declared above to JavaScript. + assert(napi_define_properties(env, + exports, + sizeof(bindings) / sizeof(bindings[0]), + bindings) == napi_ok); + + // Return the `exports` object provided. It now has two new properties, which + // are the functions we wish to expose to JavaScript. + return exports; +} +``` + +#### index.js + +```javascript +// Example illustrating the case where a native addon is loaded multiple times. +// This entire file is executed twice, concurrently - once on the main thread, +// and once on a thread launched from the main thread. + +// We load the worker threads module, which allows us to launch multiple Node.js +// environments, each in its own thread. +const { + Worker, isMainThread +} = require('worker_threads'); + +// We load the native addon. +const addon = require('bindings')('multiple_load'); + +// The iteration count can be tweaked to ensure that the output from the two +// threads is interleaved. Too few iterations and the output of one thread +// follows the output of the other, not really illustrating the concurrency. +const iterations = 1000; + +// This function is an idle loop that performs a random walk from 0 by calling +// into the native addon to either increment or decrement the initial value. +function useAddon(addon, prefix, iterations) { + if (iterations >= 0) { + if (Math.random() < 0.5) { + console.log(prefix + ': new value (decremented): ' + addon.decrement()); + } else { + console.log(prefix + ': new value (incremented): ' + addon.increment()); + } + setImmediate(() => useAddon(addon, prefix, --iterations)); + } +} + +if (isMainThread) { + // On the main thread, we launch a worker and wait for it to come online. Then + // we start the loop. + (new Worker(__filename)).on('online', + () => useAddon(addon, "Main thread", iterations)); +} else { + // On the secondary thread we immediately start the loop. + useAddon(addon, "Worker thread", iterations); +} +``` + +## Cleanup hooks + +> Note that the feature described here is currently available in N-API version 3 and later. + +Your native add-on can receive one or more notifications from the Node.js runtime engine when the context in which your native-add-on has been running is being destroyed. This gives your native add-on the opportunity to release any allocated memory before the context is destroyed by the Node.js runtime engine. + +The advantage of this technique is that your native add-on can allocate multiple pieces of memory to be associated with the context under which your native add-on is running. This can be useful if you need to allocate multiple memory buffers from different pieces of code as your native add-on is running. + +The drawback is that if you need to access these allocated buffer you are responsible for keeping track of the pointers yourself within the context your native add-on is running. Depending upon the architecture of your native add-on, this may or may not be an issue. + +### Resources + +[Cleanup on exit of the current Node.js instance](https://nodejs.org/api/n-api.html#n_api_cleanup_on_exit_of_the_current_node_js_instance) Node.js documentation for `napi_add_env_cleanup_hook` and `napi_remove_env_cleanup_hook`. + +### Example + +Because keeping track of the allocated buffers is dependent upon the architecture of the native add-on, this is a trivial example showing how the buffers can be allocated and released. + +#### binding.c + +```c++ +#include +#include +#include "node_api.h" + +namespace { + +void CleanupHook (void* arg) { + printf("cleanup(%d)\n", *static_cast(arg)); + free(arg); +} + +napi_value Init(napi_env env, napi_value exports) { + for (int i = 1; i < 5; i++) { + int* value = (int*)malloc(sizeof(*value)); + *value = i; + napi_add_env_cleanup_hook(env, CleanupHook, value); + } + return nullptr; +} + +} // anonymous namespace + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) +``` + +#### index.js + +```javascript +'use strict'; +// We load the native addon. +const addon = require('bindings')('multiple_load'); +const assert = require('assert'); +const child_process = require('child_process'); +``` + diff --git a/website/docs/special-topics/object-function-refs.md b/website/docs/special-topics/object-function-refs.md new file mode 100644 index 00000000..6262c1f6 --- /dev/null +++ b/website/docs/special-topics/object-function-refs.md @@ -0,0 +1,86 @@ +--- +id: special-topics.object-function-refs +title: Object and function references +next: special-topics.asyncworker +--- + +## Background + +JavaScript implements a dynamic memory model. When objects are no longer being used, they are automatically deleted by the garbage collector running in the background. JavaScript maintains a reference count to objects in memory to determine whether an object is still in use or not. When the reference count goes to zero, the memory for the object becomes eligible for deletion by the garbage collector. + +There are situations when you need to insure that objects created by your N-API code remain allocated. In this case you need to explicitly create a reference to it. This is the purpose of the `ObjectReference` and `ObjectFunction` classes. + +## Persistent Reference + +Object and function references can be instantiated as either `Weak` or `Persistent`. + +A `Persistent` reference initializes the internal reference count to one which prevents reclamation of the object's memory by the garbage collector. The referenced object will remain in memory for the life of the `Persistent` reference. + +Using a `Persistent` reference makes sense in the case where the duration of the reference is known ahead of time. The internal reference count is decremented when the `Persistent` reference is deleted. This will make the referenced object eligible for deletion of the internal reference count goes to zero. + +The most common case for using a `Persistent` reference is when you create a JavaScript class in your N-API code and you need to insure its constructor remains allocated by the JavaScript runtime engine. + +## Weak Reference + +For more complex implementations where multiple AsyncWorkers rely of the referenced object, it may make better sense to use a `Weak` reference which intitialiizes the internal refeence count to zero. The reference count can then be maintained using the `Reference` `Ref` and `Unref` methods. + +The most common use case for a `Weak` reference is when your N-API code needs to monitor when a JavaScript object you've created in your N-API code is released by the garbage collector. + +## ObjectReference + +The `ObjectReference` class inherits from the `Reference` class. The value it adds is a collection of `Get` and `Set` methods that manipulate the referenced object's properties. + +## FunctionReference + +Like the `ObjectReference`, the `FunctionReference` class inherits from the `Reference` class. While the `ObjectReference` class adds the `Get` and `Set` methods, the `FunctionReference` class adds a set of `Call` methods that implement calls to the function. + +## Example + +This example code shows how to use the `FunctionReference` class. + +### src/native-addon.h + +[**native-addon.h**](https://github.com/nodejs/node-addon-examples/blob/master/function-reference-demo/node-addon-api/src/native-addon.h) + +`embed:function-reference-demo/node-addon-api/src/native-addon.h` + +The `NativeAddon` C++ class has two data members that are populated in the `NativeAddon` constructor: + +- `Napi::FunctionReference jsFnRef` +- `Napi::Function jsFn` + +### src/native-addon.cc + +[**native-addon.cc**](https://github.com/nodejs/node-addon-examples/blob/master/function-reference-demo/node-addon-api/src/native-addon.cc) + +`embed:function-reference-demo/node-addon-api/src/native-addon.cc` + +The `NativeAddon` constructor, which is called from JavaScript, takes two function arguments. The first argument is stored as a `Napi::FunctionReference` and the second is stored as a `Napi::Function`. + +> *There is a deliberate error in this code.* + +The second function is stored in the `Napi::Function jsFn` data member. **This is an error** because the lifetime of the second argument is limited to the lifetime of the constructor. The value of the `jsFn` data member will be invalid after the constructor returns. The first argument is stored in the `Napi::FunctionReference jsFnRef`. Because of the use of the `Napi::FunctonReference`, the value of `jsFnRef` will remain valid after the constructor returns. + +The `NativeAddon` class implements two methods which can be called from JavaScript: `TryCallByStoredReference` and `TryCallByStoredFunction`. Notice that the `Call` method is used the same way for both the `jsFnRef` and `jsFn` data members. + +### src/binding.cc + +[**binding.cc**](https://github.com/nodejs/node-addon-examples/blob/master/function-reference-demo/node-addon-api/src/binding.cc) + +`embed:function-reference-demo/node-addon-api/src/binding.cc` + +This is a standard `binding.cc` file: + +### index.js + +[**index.js**](https://github.com/nodejs/node-addon-examples/blob/master/function-reference-demo/node-addon-api/index.js) + +`embed:function-reference-demo/node-addon-api/index.js` + +This JavaScript code shows the use of the `NativeAddon` class. Note that the call to the native `tryCallByStoredFunction` method fails because the data member on which it relies is not valid. + +## Resources + +[ObjectReference](https://github.com/nodejs/node-addon-api/blob/master/doc/object_reference.md) documentation. + +[FunctionReference](https://github.com/nodejs/node-addon-api/blob/master/doc/function_reference.md) documentation. \ No newline at end of file diff --git a/website/docs/special-topics/thread-safe-functions.md b/website/docs/special-topics/thread-safe-functions.md new file mode 100644 index 00000000..cbab3d83 --- /dev/null +++ b/website/docs/special-topics/thread-safe-functions.md @@ -0,0 +1,102 @@ +--- +id: special-topics.thread-safe-functions +title: Thread-safe functions +prev: special-topics.asyncworker +next: special-topics.context-awareness +--- + +JavaScript functions can normally only be called from a native addon's main thread. If an addon creates additional threads, then node-addon-api functions that require a `Napi::Env`, `Napi::Value`, or `Napi::Reference` must not be called from those threads. + +When an addon has additional threads and JavaScript functions need to be invoked based on the processing completed by those threads, those threads must communicate with the addon's main thread so that the main thread can invoke the JavaScript function on their behalf. The thread-safe function APIs provide an easy way to do this. + +A thread-safe function is created on the main thread via [ThreadSafeFunction::New](https://github.com/nodejs/node-addon-api/blob/master/doc/threadsafe_function.md#new): + +```cpp +New(napi_env env, + const Function& callback, + const Object& resource, + ResourceString resourceName, + size_t maxQueueSize, + size_t initialThreadCount, + ContextType* context, + Finalizer finalizeCallback, + FinalizerDataType* data); +``` + +A thread-safe function encapsulates: +- Message queue: Requests to run the JavaScript function are placed in a queue, processed asynchronously by the main thread. The amount of entries allowed in the queue before returning a "queue full" error on `NonBlockingCall()` is controlled via the `maxQueueSize` parameter (specify `0` for unlimited queue) +- JavaScript function: Callback to run (`callback` parameter). This function is either (a) automatically ran with no arguments when called via the no-argument `[Non]BlockingCall()` overloads, or (b) passed as an argument to the callback function provided in the `[Non]BlockingCall(DataType* data, Callback callback)` overloads. +- Context: Optional, arbitrary data (`context` parameter) to associate with the thread-safe function. +- Finalizer: Optional callback (`finalizeCallback` parameter) to run at destruction of the thread-safe function, when all threads have finished using it. +- Finalizer data: Optional data (`data` parameter) to provide to the finalizer callback. + +## Calling the Thread-Safe Function + +Threads may call into JavaScript via [`[Non]BlockingCall`](https://github.com/nodejs/node-addon-api/blob/master/doc/threadsafe_function.md#blockingcall--nonblockingcall). This will add an entry to the underlying thread-safe function's queue, to be handled asynchronously on the main thread during its processing of the event loop. + +## Thread Management + +Multiple threads can utilize the thread-safe function simultaneously. The thread-safe function manages its lifecycle through counting the number of threads actively utilizing it. This number starts at the initial thread count parameter in `New()`, increased via `Acquire()`, and decreased via `Released()`. Once the number of active threads reaches zero, the thread-safe function is destroyed, running the finalizer callback on the main thread if provided. + +Here are two general approaches to using thread-safe functions within applications: + +### Known Number of Threads + +If the amount of threads is known at thread-safe function creation, set the `initial_thread_count` parameter to this number in the call to `New()`. Each thread will have its own access to the thread-safe function until it calls `Release()`. Once all threads have made a call to `Release()`, the thread-safe function is destroyed. + +### Creating Threads + +Another common use-case is to dynamically create and destroy threads based on various logic at run-time. One way to handle this scenario is to expose several native JavaScript functions that interact with the thread-safe function APIs by: +1. Creating a thread-safe function via `New()` with initial thread count of `1`. +2. Calling `Acquire()` and creating a new native thread. The new thread can now use `[Non]BlockingCall()`. +3. Initiating cleanup/destruction, for example by ... + - calling `Abort()` and have each thread either call `[Non]BlockingCall()` or `Release()` + - using custom logic with other thread-safe APIs to ensure that all threads call `Release()` in order to decrease the active thread count to `0`. + +## Example + +This example node-addon-api module creates exposes a single function that creates a thread-safe function and a native thread. The function returns a promise that resolves after the native thread calls into JavaScript ten times. + +### binding.gyp + +[**binding.gyp**](https://github.com/nodejs/node-addon-examples/blob/master/thread_safe_function_counting/node-addon-api/binding.gyp) + +`embed:thread_safe_function_counting/node-addon-api/binding.gyp` + +### addon.cc + +[**addon.cc**](https://github.com/nodejs/node-addon-examples/blob/master/thread_safe_function_counting/node-addon-api/addon.cc) + +`embed:thread_safe_function_counting/node-addon-api/addon.cc` + +### addon.js + +[**addon.js**](https://github.com/nodejs/node-addon-examples/blob/master/thread_safe_function_counting/node-addon-api/addon.js) + +`embed:thread_safe_function_counting/node-addon-api/addon.js` + +Running the above script will provide output similar to: + +``` +2019-11-25T22:14:56.175Z 0 +2019-11-25T22:14:56.380Z 1 +2019-11-25T22:14:56.582Z 2 +2019-11-25T22:14:56.787Z 3 +2019-11-25T22:14:56.987Z 4 +2019-11-25T22:14:57.187Z 5 +2019-11-25T22:14:57.388Z 6 +2019-11-25T22:14:57.591Z 7 +2019-11-25T22:14:57.796Z 8 +2019-11-25T22:14:58.001Z 9 +true +``` + +## Frequently Asked Questions + +### Q: My application isn't exiting correctly. It just hangs. + +By default, Node will wait until a thread-safe function is finalized before cleaning up and exiting. See [Thread Management](#Thread-Management). This behavior can be changed via a call to `Unref()`, permitting Node to clean up without waiting for the thread count to reach zero. A call to `Ref()` will will return the threadsafe function to the previous exit behavior, requiring it to be `Release()`ed and/or `Abort()`ed by all threads utilizing it. + +### Q: If a thread receives `napi_closing` from a call to `[Non]BlockingCall()`, does it still need to call `Release()`? + +No. A return value of `napi_closing` should signify to the thread that the thread-safe function can no longer be utilized. This _includes_ the call to `Release()`. diff --git a/website/docs/toc.json b/website/docs/toc.json new file mode 100644 index 00000000..fa60a602 --- /dev/null +++ b/website/docs/toc.json @@ -0,0 +1,97 @@ +[ + { + "title": "About N-API", + "items": [ + { + "id": "about.what", + "slug": "/about/what", + "title": "What is N-API?" + }, + { + "id": "about.uses", + "slug": "/about/uses", + "title": "Uses for N-API" + } + ] + }, + { + "title": "Getting Started", + "items": [ + { + "id": "getting-started.prerequisites", + "slug": "/getting-started/prerequisites", + "title": "Prerequisites" + }, + { + "id": "getting-started.tools", + "slug": "/getting-started/tools", + "title": "The tools you'll need" + }, + { + "id": "getting-started.first", + "slug": "/getting-started/first", + "title": "A first project" + }, + { + "id": "getting-started.objectwrap", + "slug": "/getting-started/objectwrap", + "title": "Object wrap" + }, + { + "id": "getting-started.migration", + "slug": "/getting-started/migration", + "title": "Migration tutorial" + } + ] + }, + { + "title": "Build Tools", + "items": [ + { + "id": "build-tools.node-gyp", + "slug": "/build-tools/node-gyp", + "title": "node-gyp" + }, + { + "id": "build-tools.cmake-js", + "slug": "/build-tools/cmake-js", + "title": "CMake.js" + }, + { + "id": "build-tools.node-pre-gyp", + "slug": "/build-tools/node-pre-gyp", + "title": "node-pre-gyp" + }, + { + "id": "build-tools.prebuild", + "slug": "/build-tools/prebuild", + "title": "prebuild" + } + ] + }, + { + "title": "Special Topics", + "items": [ + { + "id": "special-topics.object-function-refs", + "slug": "/special-topics/object-function-refs", + "title": "Object & function refs" + }, + { + "id": "special-topics.asyncworker", + "slug": "/special-topics/asyncworker", + "title": "AsyncWorker" + }, + { + "id": "special-topics.thread-safe-functions", + "slug": "/special-topics/thread-safe-functions", + "title": "Thread-safe functions" + }, + { + "id": "special-topics.context-awareness", + "slug": "/special-topics/context-awareness", + "title": "Context awareness" + } + ] + } +] diff --git a/website/gatsby-config.js b/website/gatsby-config.js new file mode 100644 index 00000000..317156e1 --- /dev/null +++ b/website/gatsby-config.js @@ -0,0 +1,93 @@ +'use strict'; + +module.exports = { + siteMetadata: { + title: 'The N-API Resource', + sidebarTitle: 'N-API Resource', + description: 'Tutorials and more for Node.js N-API.', + siteUrl: 'https://github.com/nodejs/node-addon-examples', + keywords: 'node nodejs n-api napi tutorials native-addon', + author: { + name: 'The N-API Team', + url: 'https://github.com/nodejs/node-addon-examples', + email: '' + } + }, + pathPrefix: '/node-addon-examples', + plugins: [ + { + // keep as first gatsby-source-filesystem plugin for gatsby image support + resolve: 'gatsby-source-filesystem', + options: { + path: `${__dirname}/static/img`, + name: 'uploads' + } + }, + { + resolve: 'gatsby-source-filesystem', + options: { + name: 'content', + path: `${__dirname}/docs` + } + }, + { + resolve: 'gatsby-transformer-remark', + options: { + plugins: [ + { + resolve: `gatsby-remark-embed-snippet`, + options: { + directory: `${__dirname}/..` + }, + }, + { + resolve: 'gatsby-remark-relative-images', + options: { + name: 'uploads' + } + }, + { + resolve: 'gatsby-remark-images', + options: { + maxWidth: 704, + quality: 90, + wrapperStyle: 'margin-top: 32px; margin-bottom: 32px;', + linkImagesToOriginal: false + } + }, + { + resolve: 'gatsby-remark-responsive-iframe', + options: { + wrapperStyle: 'margin-bottom: 1rem' + } + }, + { + resolve: 'gatsby-remark-prismjs', + options: { + inlineCodeMarker: '›' + } + }, + 'gatsby-remark-copy-linked-files', + 'gatsby-remark-autolink-headers', + 'gatsby-remark-smartypants' + ] + } + }, + 'gatsby-transformer-json', + { + resolve: 'gatsby-plugin-canonical-urls', + options: { + siteUrl: 'https://github.com/nodejs/node-addon-examples' + } + }, + 'gatsby-plugin-styled-components', + 'gatsby-plugin-resolve-src', + 'gatsby-plugin-catch-links', + 'gatsby-plugin-typescript', + 'gatsby-plugin-sharp', + 'gatsby-transformer-sharp', + 'gatsby-plugin-react-helmet', + 'gatsby-plugin-netlify-cache', + 'gatsby-plugin-netlify' + ] +}; diff --git a/website/gatsby-node.js b/website/gatsby-node.js new file mode 100644 index 00000000..d2164768 --- /dev/null +++ b/website/gatsby-node.js @@ -0,0 +1,90 @@ +'use strict'; + +const path = require('path'); + +exports.onCreateNode = ({ node, actions, getNode }) => { + const { createNodeField } = actions; + + // Sometimes, optional fields tend to get not picked up by the GraphQL + // interpreter if not a single content uses it. Therefore, we're putting them + // through `createNodeField` so that the fields still exist and GraphQL won't + // trip up. An empty string is still required in replacement to `null`. + + switch (node.internal.type) { + case 'MarkdownRemark': { + const { permalink, layout } = node.frontmatter; + const { relativePath } = getNode(node.parent); + + let slug = permalink; + + if (!slug) { + if (relativePath === 'index.md') { + // If we have homepage set in docs folder, use it. + slug = '/'; + } else { + slug = `/${relativePath.replace('.md', '')}/`; + } + } + + // Used to generate URL to view this content. + createNodeField({ + node, + name: 'slug', + value: slug || '' + }); + + // Used to determine a page layout. + createNodeField({ + node, + name: 'layout', + value: layout || '' + }); + } + } +}; + +exports.createPages = async ({ graphql, actions }) => { + const { createPage } = actions; + + const allMarkdown = await graphql(` + { + allMarkdownRemark(limit: 1000) { + edges { + node { + fields { + layout + slug + } + } + } + } + } + `); + + if (allMarkdown.errors) { + console.error(allMarkdown.errors); + throw new Error(allMarkdown.errors); + } + + allMarkdown.data.allMarkdownRemark.edges.forEach(({ node }) => { + const { slug, layout } = node.fields; + + createPage({ + path: slug, + // This will automatically resolve the template to a corresponding + // `layout` frontmatter in the Markdown. + // + // Feel free to set any `layout` as you'd like in the frontmatter, as + // long as the corresponding template file exists in src/templates. + // If no template is set, it will fall back to the default `page` + // template. + // + // Note that the template has to exist first, or else the build will fail. + component: path.resolve(`./src/templates/${layout || 'page'}.tsx`), + context: { + // Data passed to context is available in page queries as GraphQL variables. + slug + } + }); + }); +}; diff --git a/website/package.json b/website/package.json new file mode 100644 index 00000000..f37639f3 --- /dev/null +++ b/website/package.json @@ -0,0 +1,86 @@ +{ + "name": "napi-resource", + "description": "Tutorials and more for Node.js N-API.", + "version": "1.0.0", + "author": "The N-API Team", + "keywords": [ + "node", + "nodejs", + "n-api", + "napi", + "tutorials", + "native-addon" + ], + "license": "MIT", + "main": "n/a", + "scripts": { + "build": "gatsby build", + "clean": "rimraf public", + "commit": "git cz", + "deploy": "gatsby build --prefix-paths && gh-pages -d public", + "dev": "gatsby develop", + "format": "prettier --write \"src/**/*.{ts,tsx,md}\"", + "lint": "tslint 'src/**/*.{ts,tsx}' -p tsconfig.json", + "start": "serve public", + "test": "npm run type-check && npm run lint", + "type-check": "tsc" + }, + "dependencies": { + "@reach/skip-nav": "^0.3.0", + "babel-plugin-styled-components": "^1.10.6", + "classnames": "^2.2.6", + "gatsby": "^2.17.7", + "gatsby-plugin-canonical-urls": "^2.1.13", + "gatsby-plugin-catch-links": "^2.1.15", + "gatsby-plugin-netlify": "^2.1.23", + "gatsby-plugin-netlify-cache": "^1.2.0", + "gatsby-plugin-react-helmet": "^3.1.13", + "gatsby-plugin-resolve-src": "^2.0.0", + "gatsby-plugin-sharp": "^2.2.36", + "gatsby-plugin-styled-components": "^3.1.11", + "gatsby-plugin-typescript": "^2.1.15", + "gatsby-remark-autolink-headers": "^2.1.16", + "gatsby-remark-copy-linked-files": "^2.1.28", + "gatsby-remark-embed-snippet": "^4.3.2", + "gatsby-remark-images": "^3.1.28", + "gatsby-remark-prismjs": "^3.3.20", + "gatsby-remark-relative-images": "^0.2.3", + "gatsby-remark-responsive-iframe": "^2.2.25", + "gatsby-remark-smartypants": "^2.1.14", + "gatsby-source-filesystem": "^2.1.35", + "gatsby-transformer-json": "^2.2.16", + "gatsby-transformer-remark": "^2.6.32", + "gatsby-transformer-sharp": "^2.3.2", + "polished": "^3.4.2", + "prism-themes": "^1.1.0", + "prismjs": "^1.16.0", + "react": "16.10.2", + "react-dom": "16.10.2", + "react-helmet": "^5.2.1", + "rehype-react": "^3.1.0", + "styled-components": "^4.4.0", + "styled-system": "^5.1.2", + "typescript": "3.6.4", + "utility-types": "^3.9.0" + }, + "devDependencies": { + "@types/classnames": "^2.2.8", + "@types/history": "^4.7.2", + "@types/node": "^12.0.10", + "@types/reach__router": "^1.2.4", + "@types/react": "^16.8.22", + "@types/react-dom": "^16.8.4", + "@types/react-helmet": "^5.0.8", + "@types/styled-components": "^4.1.20", + "@types/styled-system": "^5.1.2", + "gh-pages": "^2.2.0", + "prettier": "^1.18.2", + "rimraf": "^2.6.3", + "serve": "^11.0.2", + "tslint": "^5.18.0", + "tslint-config-kata": "^1.1.3", + "tslint-config-prettier": "^1.18.0", + "tslint-plugin-prettier": "^2.0.1", + "tslint-react": "^4.0.0" + } +} diff --git a/website/src/components/docs/DocsHeader/DocsHeader.tsx b/website/src/components/docs/DocsHeader/DocsHeader.tsx new file mode 100644 index 00000000..80d18d23 --- /dev/null +++ b/website/src/components/docs/DocsHeader/DocsHeader.tsx @@ -0,0 +1,24 @@ +import * as React from 'react'; +import { Heading, Box, Text } from 'components/foundations'; + +interface DocsHeaderProps { + title: string; + subtitle?: string; +} + +const DocsHeader: React.FC = ({ title, subtitle }) => { + return ( + + + {title} + + {subtitle && ( + + {subtitle} + + )} + + ); +}; + +export default DocsHeader; diff --git a/website/src/components/docs/DocsHeader/index.ts b/website/src/components/docs/DocsHeader/index.ts new file mode 100644 index 00000000..ec7fd69e --- /dev/null +++ b/website/src/components/docs/DocsHeader/index.ts @@ -0,0 +1 @@ +export { default as DocsHeader } from './DocsHeader'; diff --git a/website/src/components/docs/DocsWrapper/DocsWrapper.tsx b/website/src/components/docs/DocsWrapper/DocsWrapper.tsx new file mode 100644 index 00000000..04f5eef5 --- /dev/null +++ b/website/src/components/docs/DocsWrapper/DocsWrapper.tsx @@ -0,0 +1,24 @@ +import { breakpoints } from 'components/foundations/variables'; +import styled from 'styled-components'; + +interface DocsWrapperProps { + hasToc?: boolean; +} + +const DocsWrapper = styled('article')` + display: flex; + flex-direction: column; + flex: 1 1 auto; + position: relative; + padding: 32px; + + @media (min-width: ${breakpoints.lg}px) { + flex-direction: ${props => props.hasToc && 'row-reverse'}; + } + + @media (max-width: ${breakpoints.lg - 1}px) { + overflow-x: auto; + } +`; + +export default DocsWrapper; diff --git a/website/src/components/docs/DocsWrapper/index.ts b/website/src/components/docs/DocsWrapper/index.ts new file mode 100644 index 00000000..965f20d3 --- /dev/null +++ b/website/src/components/docs/DocsWrapper/index.ts @@ -0,0 +1 @@ +export { default as DocsWrapper } from './DocsWrapper'; diff --git a/website/src/components/docs/TableOfContents/TocFloatingButton.tsx b/website/src/components/docs/TableOfContents/TocFloatingButton.tsx new file mode 100644 index 00000000..75ad15aa --- /dev/null +++ b/website/src/components/docs/TableOfContents/TocFloatingButton.tsx @@ -0,0 +1,71 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors, layerIndexes, breakpoints } from 'components/foundations/variables'; + +interface ToggleableProps { + isOpen?: boolean; +} + +const Wrapper = styled('button')` + display: inline-block; + position: fixed; + bottom: 24px; + right: 24px; + padding: 0; + background-color: ${props => (props.isOpen ? colors.red06 : colors.blue06)}; + color: ${colors.white}; + cursor: pointer; + z-index: ${layerIndexes.overlay - 1}; + border-radius: 50%; + border: 1px solid rgba(255, 255, 255, 0.1); + box-shadow: 0 2px 4px 1px rgba(0, 0, 0, 0.15); + + &:hover, + &:focus { + outline: none; + } + + @media (min-width: ${breakpoints.lg}px) and (max-width: ${breakpoints.xl - 1}px) { + z-index: ${layerIndexes.dialog + 2}; + } + + @media (min-width: ${breakpoints.xl}px) { + display: none; + } +`; + +const Inner = styled('div')` + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + + &.is-open { + & svg { + transform: rotate(180deg); + } + } +`; + +interface TocFloatingButtonProps { + className?: string; + tocIsOpen?: boolean; + onClick?: (e?: React.MouseEvent) => void; +} + +const TocFloatingButton: React.SFC = ({ className, tocIsOpen, onClick }) => ( + + + + + + + +); + +export default TocFloatingButton; diff --git a/website/src/components/docs/TableOfContents/TocWrapper.tsx b/website/src/components/docs/TableOfContents/TocWrapper.tsx new file mode 100644 index 00000000..e4423dd8 --- /dev/null +++ b/website/src/components/docs/TableOfContents/TocWrapper.tsx @@ -0,0 +1,70 @@ +import styled from 'styled-components'; +import { dimensions, breakpoints, colors, layerIndexes } from 'components/foundations/variables'; + +interface ToggleableProps { + isOpen?: boolean; +} + +const TocWrapper = styled('section')` + display: block; + margin-left: 24px; + font-size: 13px; + line-height: 28px; + + @media (min-width: ${breakpoints.xl}px) { + flex: 0 0 240px; + position: sticky; + top: ${dimensions.heights.header + 32}px; + max-height: calc(100vh - ${dimensions.heights.header + 32}px); + overflow-y: auto; + } + + @media (max-width: ${breakpoints.xl - 1}px) { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100vh; + margin-left: 0; + margin-top: ${dimensions.heights.header}px; + padding: 24px; + background-color: ${colors.white}; + z-index: ${layerIndexes.overlay - 5}; + visibility: ${props => (props.isOpen ? 'visible' : 'hidden')}; + opacity: ${props => (props.isOpen ? 1 : 0)}; + transform: ${props => (props.isOpen ? 'translateY(0)' : 'translateY(64px)')}; + transition: visibility 0.3s ease, opacity 0.3s ease, transform 0.3s ease; + overflow-y: auto; + } + + @media (min-width: ${breakpoints.lg}px) and (max-width: ${breakpoints.xl - 1}px) { + margin-top: 0; + z-index: ${layerIndexes.dialog + 1}; + } + + ul { + padding-left: 16px; + border-left: 1px solid ${colors.grey02}; + list-style-type: none; + + p { + margin: 0; + } + + ul { + border-left: none; + } + } + + a { + color: ${colors.grey04}; + text-decoration: none; + + &:hover, + &:focus { + color: ${colors.grey07}; + } + } +`; + +export default TocWrapper; diff --git a/website/src/components/docs/TableOfContents/index.ts b/website/src/components/docs/TableOfContents/index.ts new file mode 100644 index 00000000..0ee9fba3 --- /dev/null +++ b/website/src/components/docs/TableOfContents/index.ts @@ -0,0 +1,2 @@ +export { default as TocWrapper } from './TocWrapper'; +export { default as TocFloatingButton } from './TocFloatingButton'; diff --git a/website/src/components/foundations/Theme.tsx b/website/src/components/foundations/Theme.tsx new file mode 100644 index 00000000..759d02bf --- /dev/null +++ b/website/src/components/foundations/Theme.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import { ThemeProvider } from 'styled-components'; +import { breakpoints, headingSizes, textSizes, colors, space, fonts, paragraphSizes } from './variables'; + +export const themeProps = { + colors, + space, + fonts, + breakpoints: [`${breakpoints.sm}px`, `${breakpoints.md}px`, `${breakpoints.lg}px`, `${breakpoints.xl}px`], + typeScale: { + heading: headingSizes, + paragraph: paragraphSizes, + text: textSizes + } +}; + +export const Theme = (props: { children: React.ReactNode }) => { + return ( + + <>{props.children} + + ); +}; + +export type TypeScale = typeof themeProps.typeScale; +export type HeadingSizes = typeof headingSizes; +export type TextSizes = typeof textSizes; +export type Color = keyof typeof themeProps['colors']; +export type Space = keyof typeof themeProps['space']; diff --git a/website/src/components/foundations/box/components/BorderBox.tsx b/website/src/components/foundations/box/components/BorderBox.tsx new file mode 100644 index 00000000..5c214787 --- /dev/null +++ b/website/src/components/foundations/box/components/BorderBox.tsx @@ -0,0 +1,40 @@ +import styled from 'styled-components'; +import { + BackgroundProps, + HeightProps, + MaxWidthProps, + SpaceProps, + borderRadius, + BorderRadiusProps, + WidthProps +} from 'styled-system'; + +import { getColor } from 'utils/helpers'; +import { Color } from 'components/foundations'; + +import { Box, BoxProps } from './Box'; + +export interface BorderBoxProps + extends BackgroundProps, + HeightProps, + MaxWidthProps, + SpaceProps, + BorderRadiusProps, + WidthProps, + BoxProps { + /** Set to `true` to enable `overflow: hidden;`. */ + noOverflow?: boolean; + /** The color key for the border. */ + borderColor?: Color; +} + +/** + * An extended `Box` with additional hooks to set border. + */ +export const BorderBox = styled(Box)` + border: 1px solid ${props => getColor(props.borderColor ? props.borderColor : 'grey02')}; + ${borderRadius}; + ${props => props.noOverflow && 'overflow: hidden;'} +`; + +BorderBox.displayName = 'BorderBox'; diff --git a/website/src/components/foundations/box/components/Box.tsx b/website/src/components/foundations/box/components/Box.tsx new file mode 100644 index 00000000..e45e2a32 --- /dev/null +++ b/website/src/components/foundations/box/components/Box.tsx @@ -0,0 +1,52 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { + layout, + LayoutProps, + position, + PositionProps, + flexbox, + FlexboxProps, + grid, + GridProps, + space, + SpaceProps, + background, + BackgroundProps, + color, + ColorProps, + typography, + TypographyProps +} from 'styled-system'; + +export interface BoxProps + extends LayoutProps, + PositionProps, + FlexboxProps, + GridProps, + SpaceProps, + BackgroundProps, + ColorProps, + TypographyProps { + /** Additional CSS classes to add to the component. */ + className?: string; + /** Additional CSS properties to add to the component. */ + style?: React.CSSProperties; +} + +/** + * Box is a view with all styled-system hooks added to it. You can use it as a + * base component for all display elements. + */ +export const Box = styled('div')` + ${layout} + ${position} + ${flexbox} + ${grid} + ${space} + ${background} + ${color} + ${typography} +`; + +Box.displayName = 'Box'; diff --git a/website/src/components/foundations/box/components/index.ts b/website/src/components/foundations/box/components/index.ts new file mode 100644 index 00000000..ca7a0476 --- /dev/null +++ b/website/src/components/foundations/box/components/index.ts @@ -0,0 +1,2 @@ +export * from './Box'; +export * from './BorderBox'; diff --git a/website/src/components/foundations/box/index.ts b/website/src/components/foundations/box/index.ts new file mode 100644 index 00000000..07635cbb --- /dev/null +++ b/website/src/components/foundations/box/index.ts @@ -0,0 +1 @@ +export * from './components'; diff --git a/website/src/components/foundations/index.ts b/website/src/components/foundations/index.ts new file mode 100644 index 00000000..d57858db --- /dev/null +++ b/website/src/components/foundations/index.ts @@ -0,0 +1,4 @@ +export * from './box'; +export * from './reset'; +export * from './typography'; +export * from './Theme'; diff --git a/website/src/components/foundations/reset/components/GlobalStyles.ts b/website/src/components/foundations/reset/components/GlobalStyles.ts new file mode 100644 index 00000000..f8ce5f67 --- /dev/null +++ b/website/src/components/foundations/reset/components/GlobalStyles.ts @@ -0,0 +1,12 @@ +import { createGlobalStyle } from 'styled-components'; +import reboot from '../styles/reboot'; +import base from '../styles/base'; +import code from '../styles/code'; + +const GlobalStyles = createGlobalStyle` +${reboot} +${base} +${code} +`; + +export default GlobalStyles; diff --git a/website/src/components/foundations/reset/components/ThemeReset.tsx b/website/src/components/foundations/reset/components/ThemeReset.tsx new file mode 100644 index 00000000..d28193e4 --- /dev/null +++ b/website/src/components/foundations/reset/components/ThemeReset.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +import { Theme } from '../../Theme'; +import GlobalStyles from './GlobalStyles'; + +interface ResetProps { + className?: string; + style?: React.CSSProperties; +} + +const ThemeReset: React.FC = ({ children }) => { + return ( + + + {children} + + ); +}; + +export default ThemeReset; diff --git a/website/src/components/foundations/reset/index.ts b/website/src/components/foundations/reset/index.ts new file mode 100644 index 00000000..bac07be5 --- /dev/null +++ b/website/src/components/foundations/reset/index.ts @@ -0,0 +1,4 @@ +import GlobalStyles from './components/GlobalStyles'; +import ThemeReset from './components/ThemeReset'; + +export { GlobalStyles, ThemeReset }; diff --git a/website/src/components/foundations/reset/styles/base.ts b/website/src/components/foundations/reset/styles/base.ts new file mode 100644 index 00000000..75677b42 --- /dev/null +++ b/website/src/components/foundations/reset/styles/base.ts @@ -0,0 +1,109 @@ +import { css } from 'styled-components'; +import { textSizes, colors } from 'components/foundations/variables'; + +const base = css` + :root { + font-size: ${textSizes[300].fontSize}px; + line-height: ${textSizes[300].lineHeight}px; + } + + html, + body, + #root { + width: 100%; + height: 100%; + } + + a { + color: ${colors.blue05}; + text-decoration: none; + + &:hover, + &:focus { + color: ${colors.indigo04}; + text-decoration: underline; + } + } + + img { + display: block; + max-width: 100%; + } + + #root { + transition: all 0.5s cubic-bezier(0.15, 1, 0.3, 1); + -webkit-transition: all 0.5s cubic-bezier(0.15, 1, 0.3, 1); + + &.pushed-legend-right { + transform: translateX(-280px); + } + } + + .noscroll { + overflow: hidden; + } + + .noselect { + user-select: none; + } + + .full-size { + height: 100%; + width: 100%; + } + + .full-size-layout { + height: 100%; + min-height: 100vh; + width: 100%; + } + + .icon-middle { + &::before { + vertical-align: middle; + } + } + + .drag-handle { + cursor: move; + display: inline-block; + + &::before { + content: '......'; + display: inline-block; + width: 10px; + word-break: break-word; + white-space: normal; + letter-spacing: 2px; + line-height: 4.5px; + text-align: center; + height: 18px; + } + } + + /* https://github.com/reach/reach-ui/blob/master/packages/skip-nav/styles.css */ + [data-reach-skip-link] { + border: 0; + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + position: absolute; + } + + [data-reach-skip-link]:focus { + padding: 1rem; + position: fixed; + top: 10px; + left: 10px; + background: white; + z-index: 100; + width: auto; + height: auto; + clip: auto; + } +`; + +export default base; diff --git a/website/src/components/foundations/reset/styles/code.ts b/website/src/components/foundations/reset/styles/code.ts new file mode 100644 index 00000000..1515dfed --- /dev/null +++ b/website/src/components/foundations/reset/styles/code.ts @@ -0,0 +1,173 @@ +import { css } from 'styled-components'; +import { colors, paragraphSizes, textSizes } from 'components/foundations/variables'; + +const code = css` + /* + + Name: Base16 Atelier Sulphurpool Light + Author: Bram de Haan (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/sulphurpool) + + Prism template by Bram de Haan (http://atelierbram.github.io/syntax-highlighting/prism/) + Original Base16 color scheme by Chris Kempson (https://github.com/chriskempson/base16) + + */ + code[class*='language-'], + pre[class*='language-'] { + font-family: Consolas, Menlo, Monaco, 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'Lucida Sans Typewriter', + 'DejaVu Sans Mono', 'Bitstream Vera Sans Mono', 'Liberation Mono', 'Nimbus Mono L', 'Courier New', Courier, + monospace; + font-size: ${textSizes[300].fontSize}px; + line-height: ${textSizes[300].lineHeight}px; + direction: ltr; + text-align: left; + white-space: pre; + word-spacing: normal; + word-break: normal; + -moz-tab-size: 4; + -o-tab-size: 4; + tab-size: 4; + -webkit-hyphens: none; + -moz-hyphens: none; + -ms-hyphens: none; + hyphens: none; + background: ${colors.grey01}; + color: ${colors.grey07}; + } + + /* Code blocks */ + pre[class*='language-'] { + padding: 1em; + margin: 0.5em 0; + overflow: auto; + } + + /* Inline code */ + :not(pre) > code[class*='language-'] { + padding: 0.1em; + border-radius: 0.3em; + background: none; + color: ${colors.blue05}; + /* font-size: ${paragraphSizes[400].fontSize}px; */ + line-height: ${paragraphSizes[400].lineHeight}px; + } + + .token.comment, + .token.prolog, + .token.doctype, + .token.cdata { + color: #898ea4; + } + + .token.punctuation { + color: #5e6687; + } + + .token.namespace { + opacity: 0.7; + } + + .token.operator, + .token.boolean, + .token.number { + color: #c76b29; + } + + .token.property { + color: #c08b30; + } + + .token.tag { + color: #3d8fd1; + } + + .token.string { + color: #22a2c9; + } + + .token.selector { + color: #6679cc; + } + + .token.attr-name { + color: #c76b29; + } + + .token.entity, + .token.url, + .language-css .token.string, + .style .token.string { + color: #22a2c9; + } + + .token.attr-value, + .token.keyword, + .token.control, + .token.directive, + .token.unit { + color: #ac9739; + } + + .token.statement, + .token.regex, + .token.atrule { + color: #22a2c9; + } + + .token.placeholder, + .token.variable { + color: #3d8fd1; + } + + .token.deleted { + text-decoration: line-through; + } + + .token.inserted { + border-bottom: 1px dotted #202746; + text-decoration: none; + } + + .token.italic { + font-style: italic; + } + + .token.important, + .token.bold { + font-weight: bold; + } + + .token.important { + color: #c94922; + } + + .token.entity { + cursor: help; + } + + pre > code.highlight { + outline: 0.4em solid #c94922; + outline-offset: 0.4em; + } + + /* overrides color-values for the Line Numbers plugin + * http://prismjs.com/plugins/line-numbers/ + */ + .line-numbers .line-numbers-rows { + border-right-color: #dfe2f1; + } + + .line-numbers-rows > span:before { + color: #979db4; + } + + /* overrides color-values for the Line Highlight plugin + * http://prismjs.com/plugins/line-highlight/ + */ + .line-highlight { + background: rgba(107, 115, 148, 0.2); + background: -webkit-linear-gradient(left, rgba(107, 115, 148, 0.2) 70%, rgba(107, 115, 148, 0)); + background: linear-gradient(to right, rgba(107, 115, 148, 0.2) 70%, rgba(107, 115, 148, 0)); + } +`; + +export default code; diff --git a/website/src/components/foundations/reset/styles/reboot.ts b/website/src/components/foundations/reset/styles/reboot.ts new file mode 100644 index 00000000..b6506d70 --- /dev/null +++ b/website/src/components/foundations/reset/styles/reboot.ts @@ -0,0 +1,356 @@ +import { css } from 'styled-components'; + +const reboot = css` + /*! + * Bootstrap Reboot v4.1.2 (https://getbootstrap.com/) + * Copyright 2011-2018 The Bootstrap Authors + * Copyright 2011-2018 Twitter, Inc. + * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) + * Forked from Normalize.css, licensed MIT (https://github.com/necolas/normalize.css/blob/master/LICENSE.md) + */ + *, + *::before, + *::after { + box-sizing: border-box; + } + + html { + font-family: sans-serif; + line-height: 1.15; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; + -ms-overflow-style: scrollbar; + -webkit-tap-highlight-color: rgba(0, 0, 0, 0); + } + + ${css` + @-ms-viewport { + width: device-width; + } + `} + + article, + aside, + figcaption, + figure, + footer, + header, + hgroup, + main, + nav, + section { + display: block; + } + + body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + font-size: 1rem; + font-weight: 400; + line-height: 1.5; + color: #212529; + text-align: left; + background-color: #fff; + } + + [tabindex='-1']:focus { + outline: 0 !important; + } + + hr { + box-sizing: content-box; + height: 0; + overflow: visible; + } + + h1, + h2, + h3, + h4, + h5, + h6 { + margin-top: 0; + margin-bottom: 0.5rem; + } + + p { + margin-top: 0; + margin-bottom: 1rem; + } + + abbr[title], + abbr[data-original-title] { + text-decoration: underline; + -webkit-text-decoration: underline dotted; + text-decoration: underline dotted; + cursor: help; + border-bottom: 0; + } + + address { + margin-bottom: 1rem; + font-style: normal; + line-height: inherit; + } + + ol, + ul, + dl { + margin-top: 0; + margin-bottom: 1rem; + } + + ol ol, + ul ul, + ol ul, + ul ol { + margin-bottom: 0; + } + + dt { + font-weight: 700; + } + + dd { + margin-bottom: 0.5rem; + margin-left: 0; + } + + blockquote { + margin:0.75rem 0; + padding:0.4rem 0 0.4rem 1.5rem; + border-left:1px solid #888; + } + + dfn { + font-style: italic; + } + + b, + strong { + font-weight: bolder; + } + + small { + font-size: 80%; + } + + sub, + sup { + position: relative; + font-size: 75%; + line-height: 0; + vertical-align: baseline; + } + + sub { + bottom: -0.25em; + } + + sup { + top: -0.5em; + } + + a { + color: #007bff; + text-decoration: none; + background-color: transparent; + -webkit-text-decoration-skip: objects; + } + + a:hover { + color: #0056b3; + text-decoration: underline; + } + + a:not([href]):not([tabindex]) { + color: inherit; + text-decoration: none; + } + + a:not([href]):not([tabindex]):hover, + a:not([href]):not([tabindex]):focus { + color: inherit; + text-decoration: none; + } + + a:not([href]):not([tabindex]):focus { + outline: 0; + } + + pre, + code, + kbd, + samp { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; + font-size: 1em; + } + + pre { + margin-top: 0; + margin-bottom: 1rem; + overflow: auto; + -ms-overflow-style: scrollbar; + } + + figure { + margin: 0 0 1rem; + } + + img { + vertical-align: middle; + border-style: none; + } + + svg:not(:root) { + overflow: hidden; + vertical-align: middle; + } + + table { + border-collapse: collapse; + } + + caption { + padding-top: 0.75rem; + padding-bottom: 0.75rem; + color: #6c757d; + text-align: left; + caption-side: bottom; + } + + th { + text-align: inherit; + } + + label { + display: inline-block; + margin-bottom: 0.5rem; + } + + button { + border-radius: 0; + } + + button:focus { + outline: 1px dotted; + outline: 5px auto -webkit-focus-ring-color; + } + + input, + button, + select, + optgroup, + textarea { + margin: 0; + font-family: inherit; + font-size: inherit; + line-height: inherit; + } + + button, + input { + overflow: visible; + } + + button, + select { + text-transform: none; + } + + button, + html [type='button'], + [type='reset'], + [type='submit'] { + -webkit-appearance: button; + } + + button::-moz-focus-inner, + [type='button']::-moz-focus-inner, + [type='reset']::-moz-focus-inner, + [type='submit']::-moz-focus-inner { + padding: 0; + border-style: none; + } + + input[type='radio'], + input[type='checkbox'] { + box-sizing: border-box; + padding: 0; + } + + input[type='date'], + input[type='time'], + input[type='datetime-local'], + input[type='month'] { + -webkit-appearance: listbox; + } + + textarea { + overflow: auto; + resize: vertical; + } + + fieldset { + min-width: 0; + padding: 0; + margin: 0; + border: 0; + } + + legend { + display: block; + width: 100%; + max-width: 100%; + padding: 0; + margin-bottom: 0.5rem; + font-size: 1.5rem; + line-height: inherit; + color: inherit; + white-space: normal; + } + + progress { + vertical-align: baseline; + } + + [type='number']::-webkit-inner-spin-button, + [type='number']::-webkit-outer-spin-button { + height: auto; + } + + [type='search'] { + outline-offset: -2px; + -webkit-appearance: none; + } + + [type='search']::-webkit-search-cancel-button, + [type='search']::-webkit-search-decoration { + -webkit-appearance: none; + } + + ::-webkit-file-upload-button { + font: inherit; + -webkit-appearance: button; + } + + output { + display: inline-block; + } + + summary { + display: list-item; + cursor: pointer; + } + + template { + display: none; + } + + [hidden] { + display: none !important; + } +`; + +export default reboot; diff --git a/website/src/components/foundations/typography/components/Heading.tsx b/website/src/components/foundations/typography/components/Heading.tsx new file mode 100644 index 00000000..4fcf6bbb --- /dev/null +++ b/website/src/components/foundations/typography/components/Heading.tsx @@ -0,0 +1,47 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +import { HeadingSizes } from '../../Theme'; +import { determineFontDimensions } from '../utils'; +import { Typography, TypographyProps } from './Typography'; + +export interface HeadingProps extends TypographyProps { + /** Additional CSS classes to add to the component. */ + className?: string; + /** Additional CSS properties to add to the component. */ + style?: React.CSSProperties; + /** What HTML element to render the text as. */ + as?: keyof JSX.IntrinsicElements | React.ComponentType; + /** Size value of the heading. */ + scale?: keyof HeadingSizes; +} + +/** + * This is a base `Text` element to handle typography elements. + */ +const StyledText = styled(Typography)` + ${props => props.scale === 100 && 'text-transform: uppercase;'} +`; + +/** + * Heading component provided as a styled component primitive. + */ +export const Heading: React.SFC = ({ children, as, scale: size, color, ...rest }) => ( + + {children} + +); + +Heading.defaultProps = { + as: 'h2', + color: 'grey09', + scale: 800 +}; + +Heading.displayName = 'Heading'; diff --git a/website/src/components/foundations/typography/components/Link.tsx b/website/src/components/foundations/typography/components/Link.tsx new file mode 100644 index 00000000..597b3e9f --- /dev/null +++ b/website/src/components/foundations/typography/components/Link.tsx @@ -0,0 +1,20 @@ +import * as React from 'react'; + +import { Omit } from 'utils/types'; +import { determineFontDimensions } from '../utils'; +import { TextProps, Text } from './Text'; + +export interface LinkProps extends React.AnchorHTMLAttributes, Omit {} + +/** + * Link component provided as a styled component primitive. + */ +export const Link: React.SFC = ({ children, scale, ...rest }) => { + return ( + + {children} + + ); +}; + +Link.displayName = 'Link'; diff --git a/website/src/components/foundations/typography/components/Paragraph.tsx b/website/src/components/foundations/typography/components/Paragraph.tsx new file mode 100644 index 00000000..33362182 --- /dev/null +++ b/website/src/components/foundations/typography/components/Paragraph.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +import { TextSizes } from '../../Theme'; +import { determineFontDimensions } from '../utils'; +import { Typography, TypographyProps } from './Typography'; + +/** + * This is a base `Text` element to handle typography elements. + */ +const StyledText = styled(Typography)` + letter-spacing: -0.05px; +`; + +export interface ParagraphProps extends TypographyProps { + /** Additional CSS classes to add to the component. */ + className?: string; + /** Additional CSS properties to add to the component. */ + style?: React.CSSProperties; + /** What HTML element to render the text as. */ + as?: keyof JSX.IntrinsicElements | React.ComponentType; + /** Size value of the text. */ + scale?: keyof TextSizes; +} + +/** + * Paragraph component provided as a styled component primitive. + */ +export const Paragraph: React.SFC = ({ children, as, scale, ...rest }) => ( + + {children} + +); + +Paragraph.defaultProps = { + as: 'p', + color: 'grey07', + scale: 300 +}; + +Paragraph.displayName = 'Paragraph'; diff --git a/website/src/components/foundations/typography/components/Text.tsx b/website/src/components/foundations/typography/components/Text.tsx new file mode 100644 index 00000000..5654c63d --- /dev/null +++ b/website/src/components/foundations/typography/components/Text.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import styled from 'styled-components'; + +import { TextSizes } from '../../Theme'; +import { determineFontDimensions } from '../utils'; +import { Typography, TypographyProps } from './Typography'; + +/** + * This is a base `Text` element to handle typography elements. + */ +const StyledText = styled(Typography)` + letter-spacing: -0.05px; +`; + +export interface TextProps extends TypographyProps { + /** Additional CSS classes to add to the component. */ + className?: string; + /** Additional CSS properties to add to the component. */ + style?: React.CSSProperties; + /** What HTML element to render the text as. */ + as?: keyof JSX.IntrinsicElements | React.ComponentType; + /** Size value of the text. */ + scale?: keyof TextSizes; +} + +/** + * Text component provided as a styled component primitive. + */ +export const Text: React.SFC = ({ children, as, scale: size, ...rest }) => ( + + {children} + +); + +Text.defaultProps = { + as: 'span', + scale: 300 +}; + +Text.displayName = 'Text'; diff --git a/website/src/components/foundations/typography/components/Typography.tsx b/website/src/components/foundations/typography/components/Typography.tsx new file mode 100644 index 00000000..de094d52 --- /dev/null +++ b/website/src/components/foundations/typography/components/Typography.tsx @@ -0,0 +1,19 @@ +import styled from 'styled-components'; +import { layout, LayoutProps, space, SpaceProps, color, ColorProps, typography, TypographyProps } from 'styled-system'; + +export interface TypographyProps extends LayoutProps, SpaceProps, ColorProps, TypographyProps { + /** Extended color props. */ + color?: string; +} + +/** + * This is a base `Text` element to handle typography elements. + */ +export const Typography = styled('span')` + ${layout} + ${space} + ${color} + ${typography} +`; + +Typography.displayName = 'Typography'; diff --git a/website/src/components/foundations/typography/index.ts b/website/src/components/foundations/typography/index.ts new file mode 100644 index 00000000..1cec9890 --- /dev/null +++ b/website/src/components/foundations/typography/index.ts @@ -0,0 +1,6 @@ +export * from './components/Heading'; +export * from './components/Link'; +export * from './components/Paragraph'; +export * from './components/Text'; + +export * from './utils'; diff --git a/website/src/components/foundations/typography/utils/determineFontDimensions.tsx b/website/src/components/foundations/typography/utils/determineFontDimensions.tsx new file mode 100644 index 00000000..6df9654a --- /dev/null +++ b/website/src/components/foundations/typography/utils/determineFontDimensions.tsx @@ -0,0 +1,31 @@ +import { themeProps, TypeScale } from '../../Theme'; + +/** + * Determines font sizes based on the text type and size index. + * + * @param textType Either `text` or `heading`. + * @param scale The size key. + */ +export function determineFontDimensions(textType: keyof TypeScale, scale: number = 400) { + const match: any = (themeProps.typeScale[textType] as any)[scale]; + + if (textType === 'heading') { + const styleProps = { + fontSize: `${match.fontSize}px`, + lineHeight: `${match.lineHeight}px`, + fontWeight: scale <= 400 ? 600 : 500, + letterSpacing: `${match.letterSpacing}px` + }; + + return { + ...styleProps, + ...(scale === 100 ? { textTransform: 'uppercase' } : {}) + }; + } + + return { + fontSize: `${match.fontSize}px`, + lineHeight: `${match.lineHeight}px`, + fontWeight: 400 + }; +} diff --git a/website/src/components/foundations/typography/utils/index.ts b/website/src/components/foundations/typography/utils/index.ts new file mode 100644 index 00000000..57b5a18f --- /dev/null +++ b/website/src/components/foundations/typography/utils/index.ts @@ -0,0 +1 @@ +export * from './determineFontDimensions'; diff --git a/website/src/components/foundations/variables.ts b/website/src/components/foundations/variables.ts new file mode 100644 index 00000000..42bb76cd --- /dev/null +++ b/website/src/components/foundations/variables.ts @@ -0,0 +1,242 @@ +/** + * Example color values. The default here uses the colours used by the Aksara + * design system. You can modify these according to your needs. + */ +export const colors = { + // Blue + blue01: '#e7f1fc', + blue02: '#b9d7f8', + blue03: '#8bbdf3', + blue04: '#5ca3ef', + blue05: '#2e89ea', + blue06: '#006fe6', + blue07: '#005bbd', + blue08: '#004793', + blue09: '#003369', + blue10: '#001f3f', + + // Indigo + indigo01: '#e7eaf4', + indigo02: '#b9c0df', + indigo03: '#8b97c9', + indigo04: '#5c6db4', + indigo05: '#2e449f', + indigo06: '#001b8a', + indigo07: '#001771', + indigo08: '#001258', + indigo09: '#000d3f', + indigo10: '#000826', + + // Turquoise + turquoise01: '#e7fafd', + turquoise02: '#b9f1f9', + turquoise03: '#8be7f5', + turquoise04: '#5cdef1', + turquoise05: '#2ed5ed', + turquoise06: '#00cce9', + turquoise07: '#00a7bf', + turquoise08: '#008295', + turquoise09: '#005d6a', + turquoise10: '#003840', + + // Green + green01: '#f2f8f0', + green02: '#daecd3', + green03: '#c1e0b7', + green04: '#a9d49a', + green05: '#90c87d', + green06: '#78bc61', + green07: '#639a50', + green08: '#4d783e', + green09: '#37562d', + green10: '#21341b', + + // Yellow + yellow01: '#fff7ed', + yellow02: '#ffe7ca', + yellow03: '#ffd7a8', + yellow04: '#ffc885', + yellow05: '#ffb862', + yellow06: '#ffa940', + yellow07: '#d18b35', + yellow08: '#a36c29', + yellow09: '#744d1e', + yellow10: '#462f12', + + // Red + red01: '#fce9e8', + red02: '#f7bfbc', + red03: '#f2958f', + red04: '#ed6b63', + red05: '#e84136', + red06: '#e3170a', + red07: '#ba1309', + red08: '#910f07', + red09: '#680b05', + red10: '#3e0703', + + // Grey + grey01: '#fafafa', + grey02: '#edeeee', + grey03: '#cacece', + grey04: '#858e8d', + grey05: '#626e6d', + grey06: '#404e4d', + grey07: '#293232', + grey08: '#1e2423', + grey09: '#121615', + grey10: '#060807', + + // Helper colors + white: '#fff', + black: '#000' +}; + +export const systemFonts = + "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'"; + +export const fonts = { + system: systemFonts, + // Example for including additional fonts to the default sans-serif stack. + sansSerif: `Barlow, ${systemFonts}`, + monospace: "'SF Mono', Inconsolata, Menlo, Monaco, Consolas, 'Courier New', Courier, monospace;" +}; + +/** Heading size values mapped by size number. */ +export const headingSizes = { + 900: { + fontSize: 42, + lineHeight: 48, + letterSpacing: -0.2 + }, + 800: { + fontSize: 35, + lineHeight: 40, + letterSpacing: -0.2 + }, + 700: { + fontSize: 29, + lineHeight: 32, + letterSpacing: -0.2 + }, + 600: { + fontSize: 24, + lineHeight: 28, + letterSpacing: -0.05 + }, + 500: { + fontSize: 20, + lineHeight: 24, + letterSpacing: -0.05 + }, + 400: { + fontSize: 16, + lineHeight: 20, + letterSpacing: -0.05 + }, + 300: { + fontSize: 14, + lineHeight: 20, + letterSpacing: -0.05 + }, + 200: { + fontSize: 12, + lineHeight: 16, + letterSpacing: 0 + }, + 100: { + fontSize: 12, + lineHeight: 16, + letterSpacing: 0.5 + } +}; + +/** Text size values mapped by size number. */ +export const textSizes = { + 500: { + fontSize: 20, + lineHeight: 24 + }, + 400: { + fontSize: 16, + lineHeight: 20 + }, + 300: { + fontSize: 14, + lineHeight: 20 + }, + 200: { + fontSize: 12, + lineHeight: 16 + } +}; + +/** Text size values mapped by size number. */ +export const paragraphSizes = { + 400: { + fontSize: 16, + lineHeight: 24 + }, + 300: { + fontSize: 14, + lineHeight: 24 + } +}; + +/** Space values (in px) mapped by size designators */ +export const space = { + /** Equivalent to 2px */ + xxxs: 2, + /** Equivalent to 4px */ + xxs: 4, + /** Equivalent to 8px */ + xs: 8, + /** Equivalent to 12px */ + sm: 12, + /** Equivalent to 16px */ + md: 16, + /** Equivalent to 24px */ + lg: 24, + /** Equivalent to 32px */ + xl: 32, + /** Equivalent to 48px */ + xxl: 48 +}; + +/** Breakpoint values (in px) mapped by size designators */ +export const breakpoints = { + /** 0px to 319px */ + xs: 0, + /** 320px to 767px */ + sm: 320, + /** 768px to 1023px */ + md: 768, + /** 1024px to 1439px */ + lg: 1024, + /** 1440px and above */ + xl: 1440 +}; + +export const layerIndexes = { + base: 0, + flat: 1, + floating: 2, + stickyNav: 8, + overlay: 16, + dialog: 24, + popout: 32 +}; + +export const dimensions = { + widths: { + sidebar: { + sm: 240, + md: 280, + lg: 200 + }, + containerPadding: space.lg + }, + heights: { + header: 64 + } +}; diff --git a/website/src/components/layout/Container/Container.tsx b/website/src/components/layout/Container/Container.tsx new file mode 100644 index 00000000..a488a535 --- /dev/null +++ b/website/src/components/layout/Container/Container.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +const Container = styled('div')` + position: relative; + margin-left: auto; + margin-right: auto; + width: 100%; + max-width: 704px; +`; + +export default Container; diff --git a/website/src/components/layout/Container/index.ts b/website/src/components/layout/Container/index.ts new file mode 100644 index 00000000..10cc20c6 --- /dev/null +++ b/website/src/components/layout/Container/index.ts @@ -0,0 +1 @@ +export { default as Container } from './Container'; diff --git a/website/src/components/layout/Footer/Footer.tsx b/website/src/components/layout/Footer/Footer.tsx new file mode 100644 index 00000000..1eca664c --- /dev/null +++ b/website/src/components/layout/Footer/Footer.tsx @@ -0,0 +1,32 @@ +import React from 'react'; +import styled from 'styled-components'; +import { colors } from 'components/foundations/variables'; +import { Paragraph } from 'components/foundations'; + +const Wrapper = styled('footer')` + padding-top: 24px; + border-top: 1px solid ${colors.grey02}; +`; + +const Footer: React.SFC = () => ( + + +
+ Comments or suggestions about this page?{' '} + + Let us know + + . +
+ + Created with{' '} + + Grundgesetz + + . + +
+
+); + +export default Footer; diff --git a/website/src/components/layout/Footer/FooterWrapper.tsx b/website/src/components/layout/Footer/FooterWrapper.tsx new file mode 100644 index 00000000..3d3afbc6 --- /dev/null +++ b/website/src/components/layout/Footer/FooterWrapper.tsx @@ -0,0 +1,8 @@ +import styled from 'styled-components'; +import { space } from 'components/foundations/variables'; + +const FooterWrapper = styled('div')` + margin-top: ${space.xxl}px; +`; + +export default FooterWrapper; diff --git a/website/src/components/layout/Footer/index.ts b/website/src/components/layout/Footer/index.ts new file mode 100644 index 00000000..15a58996 --- /dev/null +++ b/website/src/components/layout/Footer/index.ts @@ -0,0 +1,2 @@ +export { default as Footer } from './Footer'; +export { default as FooterWrapper } from './FooterWrapper'; diff --git a/website/src/components/layout/Header/Header.tsx b/website/src/components/layout/Header/Header.tsx new file mode 100644 index 00000000..e4ec30f6 --- /dev/null +++ b/website/src/components/layout/Header/Header.tsx @@ -0,0 +1,39 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import { breakpoints, colors, dimensions, layerIndexes } from 'components/foundations/variables'; + +interface HeaderProps { + navigation?: boolean; + absolute?: boolean; + fixed?: boolean; +} + +const isFixed = css` + @media (min-width: ${breakpoints.lg}px) { + left: ${dimensions.widths.sidebar.lg}px; + } +`; + +const Wrapper = styled('header')` + display: flex; + flex-direction: column; + position: ${props => (props.fixed ? 'fixed' : props.absolute ? 'absolute' : 'relative')}; + top: 0; + left: 0; + width: 100%; + height: ${dimensions.heights.header}px; + padding: 0 24px; + background-color: ${props => (props.navigation ? colors.grey01 : colors.white)}; + border-bottom: ${props => (props.navigation ? 'none' : `1px solid ${colors.grey02}`)}; + z-index: ${layerIndexes.stickyNav}; + + ${props => props.fixed && isFixed} +`; + +const Header: React.SFC = ({ children, absolute, fixed, navigation }) => ( + + {children} + +); + +export default Header; diff --git a/website/src/components/layout/Header/HeaderInner.tsx b/website/src/components/layout/Header/HeaderInner.tsx new file mode 100644 index 00000000..6735789e --- /dev/null +++ b/website/src/components/layout/Header/HeaderInner.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import { breakpoints } from 'components/foundations/variables'; + +interface HeaderInnerProps { + className?: string; + contents?: 'space-around' | 'space-between' | 'space-evenly' | 'flex-start' | 'flex-end'; + hideOnMobile?: boolean; + hideOnDesktop?: boolean; +} + +const HideOnMobile = css` + @media (max-width: ${breakpoints.lg - 1}px) { + display: none; + } +`; + +const HideOnDesktop = css` + @media (min-width: ${breakpoints.lg}px) { + display: none; + } +`; + +const Wrapper = styled('div')` + display: flex; + flex-direction: row; + align-items: center; + flex: 1; + justify-content: ${props => props.contents}; + + ${props => props.hideOnMobile && HideOnMobile} + ${props => props.hideOnDesktop && HideOnDesktop} +`; + +const HeaderInner: React.SFC = ({ children, className, contents, ...rest }) => ( + + {children} + +); + +HeaderInner.defaultProps = { + className: undefined, + contents: 'space-between' +}; + +export default HeaderInner; diff --git a/website/src/components/layout/Header/index.ts b/website/src/components/layout/Header/index.ts new file mode 100644 index 00000000..a5ae1120 --- /dev/null +++ b/website/src/components/layout/Header/index.ts @@ -0,0 +1,2 @@ +export { default as Header } from './Header'; +export { default as HeaderInner } from './HeaderInner'; diff --git a/website/src/components/layout/LayoutMain/LayoutMain.tsx b/website/src/components/layout/LayoutMain/LayoutMain.tsx new file mode 100644 index 00000000..02773fac --- /dev/null +++ b/website/src/components/layout/LayoutMain/LayoutMain.tsx @@ -0,0 +1,136 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Link } from 'gatsby'; +import { SkipNavContent } from '@reach/skip-nav'; + +import { NavigationContext, NavigationActionTypes } from '../Navigation/NavigationContext'; +import { Header, HeaderInner } from '../Header'; +import { NavButton } from '../Navigation'; +import { Edge, HeaderMenuItem } from 'interfaces/nodes'; +import { breakpoints, dimensions, colors, textSizes } from 'components/foundations/variables'; +import { isActive } from 'utils/helpers'; +import { determineFontDimensions } from 'components/foundations'; + +interface LayoutMainInnerProps { + className?: string; + isNavigationOpen?: boolean; +} + +interface LayoutMainProps extends LayoutMainInnerProps { + title: string; + headerMenus?: Edge[]; +} + +interface FontSizeProps { + size: ReturnType; +} + +const StyledLayoutMain = styled('div')` + display: flex; + flex-direction: column; + flex: 1 1 auto; + position: relative; + transition: margin-left 0.3s ease; + + @media (min-width: ${breakpoints.lg}px) { + margin-left: ${dimensions.widths.sidebar.lg}px; + } +`; + +const LogoWrapper = styled('div')` + display: flex; + align-items: center; + justify-content: center; + flex: 1; + margin: 0 24px; +`; + +const DocumentationMenu = styled('nav')` + display: flex; + flex-direction: row; + + a { + padding: 8px 0; + color: ${colors.grey07}; + font-size: ${textSizes[300].fontSize}px; + line-height: ${textSizes[300].lineHeight}px; + font-weight: 600; + + &:hover, + &:focus, + &.active { + color: ${colors.blue07}; + text-decoration: none; + outline: none; + } + + &:not(:first-child) { + margin-left: 24px; + } + } +`; + +const HomepageLink = styled(Link)` + color: ${colors.grey09}; + font-size: ${props => props.size.fontSize}; + line-height: ${props => props.size.lineHeight}; + font-weight: ${props => props.size.fontWeight}; + + &:hover, + &:focus { + color: ${colors.grey09}; + text-decoration: none; + } +`; + +const LayoutMain: React.SFC = ({ children, title, className, headerMenus }) => { + const { state, dispatch } = React.useContext(NavigationContext); + + return ( + +
+ + dispatch({ type: NavigationActionTypes.TOGGLE_DRAWER })} + > + Toggle Drawer + + + dispatch({ type: NavigationActionTypes.CLOSE_DRAWER })} + > + {title} + + + + + + {headerMenus && + headerMenus.map(({ node }) => { + if (node.external) { + return ( + + {node.label} + + ); + } + + return ( + + {node.label} + + ); + })} + + +
+ {children} +
+ ); +}; + +export default LayoutMain; diff --git a/website/src/components/layout/LayoutMain/index.tsx b/website/src/components/layout/LayoutMain/index.tsx new file mode 100644 index 00000000..e0efda8d --- /dev/null +++ b/website/src/components/layout/LayoutMain/index.tsx @@ -0,0 +1 @@ +export { default as LayoutMain } from './LayoutMain'; diff --git a/website/src/components/layout/LayoutRoot/LayoutRoot.tsx b/website/src/components/layout/LayoutRoot/LayoutRoot.tsx new file mode 100644 index 00000000..db34bc11 --- /dev/null +++ b/website/src/components/layout/LayoutRoot/LayoutRoot.tsx @@ -0,0 +1,79 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Helmet } from 'react-helmet'; +import { graphql, StaticQuery } from 'gatsby'; +import { WindowLocation } from '@reach/router'; +import { SkipNavLink } from '@reach/skip-nav'; + +import { NavigationContextProvider } from 'components/layout/Navigation/NavigationContext'; + +import { SiteMetadata } from 'interfaces/gatsby'; +import { breakpoints } from 'components/foundations/variables'; + +const StyledLayoutRoot = styled('div')` + display: flex; + flex-direction: column; + min-height: 100vh; + + @media (min-width: ${breakpoints.md}px) { + flex-direction: row; + } +`; + +interface LayoutRootProps { + className?: string; + location?: WindowLocation; +} + +interface DataProps { + site: { + siteMetadata: SiteMetadata; + }; +} + +const LayoutRoot: React.SFC = ({ children, className, location }) => ( + + + {(data: DataProps) => { + const { siteMetadata } = data.site; + + return ( + + + {siteMetadata.title} + + + + + + + + + {children} + + ); + }} + + +); + +export default LayoutRoot; + +const query = graphql` + query LayoutRootQuery { + site { + siteMetadata { + title + sidebarTitle + description + siteUrl + keywords + author { + name + url + email + } + } + } + } +`; diff --git a/website/src/components/layout/LayoutRoot/index.tsx b/website/src/components/layout/LayoutRoot/index.tsx new file mode 100644 index 00000000..d708beca --- /dev/null +++ b/website/src/components/layout/LayoutRoot/index.tsx @@ -0,0 +1 @@ +export { default as LayoutRoot } from './LayoutRoot'; diff --git a/website/src/components/layout/Navigation/NavButton.tsx b/website/src/components/layout/Navigation/NavButton.tsx new file mode 100644 index 00000000..822aa0aa --- /dev/null +++ b/website/src/components/layout/Navigation/NavButton.tsx @@ -0,0 +1,94 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { colors } from 'components/foundations/variables'; + +type NavButtonProps = React.ButtonHTMLAttributes & { + height?: number; + width?: number; + fill?: string; + icon?: 'hamburger' | 'x'; +}; + +const Root = styled('button')` + display: inline-block; + margin: 0; + padding: 0; + height: 100%; + background: none; + border: none; + color: inherit; + cursor: pointer; + + &:hover, + &:focus { + outline: none; + } +`; + +const VisuallyHidden = styled('span')` + border: 0; + clip: rect(0 0 0 0); + height: 1px; + width: 1px; + margin: -1px; + padding: 0; + overflow: hidden; + position: absolute; +`; + +const NavButton: React.FC = ({ height, width, fill, icon, children, ...rest }) => { + if (icon === 'hamburger') { + return ( + + {children} + + + + + + + + + + + + + + ); + } + + return ( + + {children} + + + + + + + ); +}; + +NavButton.defaultProps = { + height: 24, + width: 24, + fill: colors.grey05, + icon: 'hamburger' +}; + +export default NavButton; diff --git a/website/src/components/layout/Navigation/Navigation.tsx b/website/src/components/layout/Navigation/Navigation.tsx new file mode 100644 index 00000000..b857e548 --- /dev/null +++ b/website/src/components/layout/Navigation/Navigation.tsx @@ -0,0 +1,221 @@ +import React from 'react'; +import styled, { css } from 'styled-components'; +import { Link } from 'gatsby'; + +import { MenuNode, Edge, HeaderMenuItem } from 'interfaces/nodes'; +import { determineFontDimensions, Heading } from 'components/foundations'; +import { colors, layerIndexes, breakpoints, dimensions } from 'components/foundations/variables'; +import { isActive } from 'utils/helpers'; + +import { NavigationContext, NavigationActionTypes } from './NavigationContext'; +import NavigationMenu from './NavigationMenu'; +import NavButton from './NavButton'; + +interface ToggleableProps { + isOpen?: boolean; +} + +const Wrapper = styled('aside')` + position: fixed; + transition: all 0.3s ease; + background-color: ${colors.white}; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + z-index: ${layerIndexes.dialog}; + overflow-y: auto; + + @media (min-width: ${breakpoints.md}px) and (max-width: ${breakpoints.lg - 1}px) { + width: ${dimensions.widths.sidebar.sm}px; + box-shadow: none; + border-bottom: none; + } + + @media (max-width: ${breakpoints.lg - 1}px) { + position: fixed; + top: 0px; + left: 0px; + bottom: 0px; + right: 0px; + width: ${dimensions.widths.sidebar.md}px; + height: 100vh; + padding-bottom: 5rem; + overflow-y: auto; + pointer-events: auto; + transform: translate(${props => (props.isOpen ? '0' : '-100%')}, 0); + transition: transform 0.3s ease; + } + + @media (min-width: ${breakpoints.lg}px) { + flex: 0 0 ${dimensions.widths.sidebar.lg}px; + box-shadow: none; + border-bottom: none; + background-color: ${colors.grey01}; + } +`; + +const WrapperInner = styled('nav')` + margin-top: ${dimensions.heights.header}px; + + @media (min-width: ${breakpoints.lg}px) { + width: 200px; + flex: 1 1 auto; + z-index: 2; + height: 100vh; + overflow-y: auto; + } +`; + +const Header = styled('div')` + display: flex; + flex-direction: column; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: ${dimensions.heights.header}px; + padding: 0 24px; + background-color: ${colors.white}; + border-bottom: 1px solid ${colors.grey02}; + z-index: ${layerIndexes.stickyNav}; + + @media (min-width: ${breakpoints.lg}px) { + border-bottom-color: transparent; + background-color: ${colors.grey01}; + } +`; + +interface HeaderInnerProps { + hideOnMobile?: boolean; + hideOnDesktop?: boolean; +} + +const HideOnMobile = css` + @media (max-width: ${breakpoints.lg - 1}px) { + display: none; + } +`; + +const HideOnDesktop = css` + @media (min-width: ${breakpoints.lg}px) { + display: none; + } +`; + +const HeaderInner = styled('div')` + display: flex; + flex-direction: row; + flex: 1; + align-items: center; + justify-content: space-between; + + ${props => props.hideOnMobile && HideOnMobile} + ${props => props.hideOnDesktop && HideOnDesktop} +`; + +interface FontSizeProps { + size: ReturnType; +} + +const HomepageLink = styled(Link)` + color: ${colors.grey09}; + font-size: ${props => props.size.fontSize}; + font-size: ${props => props.size.lineHeight}; + font-weight: ${props => props.size.fontWeight}; + + &:hover, + &:focus { + color: ${colors.grey09}; + text-decoration: none; + } +`; + +const DocumentationMenu = styled('div')` + display: flex; + flex-direction: column; + padding: 16px 24px; + border-bottom: 1px solid ${colors.grey02}; + + a { + padding: 8px 0; + color: ${colors.grey07}; + + &:hover, + &:focus, + &.active { + color: ${colors.blue07}; + text-decoration: none; + outline: none; + } + } + + ${HideOnDesktop} +`; + +const DocumentationNav = styled('div')` + display: flex; + flex-direction: column; + padding: 24px; +`; + +interface NavigationProps { + title: string; + navigation?: Edge[]; + headerMenus?: Edge[]; +} + +function Navigation({ title, navigation, headerMenus }: NavigationProps) { + const { state, dispatch } = React.useContext(NavigationContext); + + return ( + +
+ + dispatch({ type: NavigationActionTypes.CLOSE_DRAWER })} + > + {title} + + + + + Menu + + dispatch({ type: NavigationActionTypes.TOGGLE_DRAWER })} + > + Toggle Drawer + + +
+ + + {headerMenus && + headerMenus.map(({ node }) => { + if (node.external) { + return ( + + {node.label} + + ); + } + + return ( + + {node.label} + + ); + })} + + dispatch({ type: NavigationActionTypes.TOGGLE_DRAWER })}> + {navigation && + navigation.map(({ node }) => )} + + +
+ ); +} + +export default Navigation; diff --git a/website/src/components/layout/Navigation/NavigationContext.tsx b/website/src/components/layout/Navigation/NavigationContext.tsx new file mode 100644 index 00000000..7112d743 --- /dev/null +++ b/website/src/components/layout/Navigation/NavigationContext.tsx @@ -0,0 +1,55 @@ +import * as React from 'react'; +import { $Values } from 'utility-types'; + +interface NavigationState { + isOpen: boolean; +} + +interface NavigationContextValue { + state: NavigationState; + dispatch: React.Dispatch; +} + +const initialState = { + isOpen: false +}; + +const NavigationActionTypes = { + RESET: 'NavigationContext/RESET', + OPEN_DRAWER: 'NavigationContext/OPEN_DRAWER', + CLOSE_DRAWER: 'NavigationContext/CLOSE_DRAWER', + TOGGLE_DRAWER: 'NavigationContext/TOGGLE_DRAWER' +}; + +// The only way TypeScript allows us to make a type-safe context value. +const NavigationContext = React.createContext({} as NavigationContextValue); + +interface NavigationActions { + type: $Values; +} + +const reducer: React.Reducer = (state, action) => { + switch (action.type) { + case NavigationActionTypes.RESET: + return initialState; + case NavigationActionTypes.OPEN_DRAWER: + return { ...state, isOpen: true }; + case NavigationActionTypes.CLOSE_DRAWER: + return { ...state, isOpen: false }; + case NavigationActionTypes.TOGGLE_DRAWER: + return { ...state, isOpen: !state.isOpen }; + default: + return state; + } +}; + +function NavigationContextProvider(props: { children: React.ReactNode }) { + const [state, dispatch] = React.useReducer(reducer, initialState); + const value = { state, dispatch }; + + return {props.children}; +} + +const NavigationContextConsumer = NavigationContext.Consumer; + +export { NavigationContext, NavigationActionTypes, NavigationContextProvider, NavigationContextConsumer }; diff --git a/website/src/components/layout/Navigation/NavigationMenu.tsx b/website/src/components/layout/Navigation/NavigationMenu.tsx new file mode 100644 index 00000000..ad076ac4 --- /dev/null +++ b/website/src/components/layout/Navigation/NavigationMenu.tsx @@ -0,0 +1,78 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Link } from 'gatsby'; + +import { MenuNode } from 'interfaces/nodes'; +import { Heading, Box } from 'components/foundations'; +import { colors, space } from 'components/foundations/variables'; +import { isActive } from 'utils/helpers'; + +interface NavigationMenuProps { + node: MenuNode; + menuKey: string; +} + +interface ToggleableProps { + isOpen?: boolean; +} + +const ToggleMenu = styled('ul')` + list-style-type: none; + margin: 0 -${space.xs}px; + padding: 0; + transition: all 0.3s ease; +`; + +const ToggleMenuList = styled('li')` + margin: 0; + font-size: 85%; + color: ${colors.grey07}; + + a { + display: block; + padding: ${space.xs}px; + border: 2px solid transparent; + border-radius: 2px; + color: ${colors.grey07}; + + &:hover, + &:focus { + background-color: ${colors.grey02}; + color: ${colors.grey07}; + text-decoration: none; + } + + &:focus { + outline: none; + background-color: ${colors.blue01}; + border-color: ${colors.blue05}; + } + + &.active { + color: ${colors.grey07}; + background-color: ${colors.blue01}; + border-color: transparent; + } + } +`; + +const NavigationMenu: React.FC = ({ node }) => { + return ( + + + {node.title} + + + {node.items.map(item => ( + + + {item.title} + + + ))} + + + ); +}; + +export default React.memo(NavigationMenu); diff --git a/website/src/components/layout/Navigation/index.ts b/website/src/components/layout/Navigation/index.ts new file mode 100644 index 00000000..bc8fc38c --- /dev/null +++ b/website/src/components/layout/Navigation/index.ts @@ -0,0 +1,4 @@ +import Navigation from './Navigation'; +import NavButton from './NavButton'; + +export { Navigation, NavButton }; diff --git a/website/src/components/layout/Overlay/Overlay.tsx b/website/src/components/layout/Overlay/Overlay.tsx new file mode 100644 index 00000000..41cb5df8 --- /dev/null +++ b/website/src/components/layout/Overlay/Overlay.tsx @@ -0,0 +1,39 @@ +import * as React from 'react'; +import styled, { css } from 'styled-components'; + +import { NavigationContext } from '../Navigation/NavigationContext'; +import { breakpoints, layerIndexes } from 'components/foundations/variables'; + +interface OverlayProps { + visible?: boolean; +} + +const Visible = css` + @media (max-width: ${breakpoints.lg - 1}px) { + opacity: 1; + visibility: visible; + } +`; + +const Root = styled('div')` + position: fixed; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: rgba(0, 31, 63, 0.6); + z-index: ${layerIndexes.overlay}; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + + ${props => props.visible && Visible} +`; + +const Overlay: React.FC = () => { + const { state } = React.useContext(NavigationContext); + + return ; +}; + +export default Overlay; diff --git a/website/src/components/layout/Overlay/index.ts b/website/src/components/layout/Overlay/index.ts new file mode 100644 index 00000000..40a769a8 --- /dev/null +++ b/website/src/components/layout/Overlay/index.ts @@ -0,0 +1 @@ +export { default as Overlay } from './Overlay'; diff --git a/website/src/components/layout/Page/NotFoundWrapper.tsx b/website/src/components/layout/Page/NotFoundWrapper.tsx new file mode 100644 index 00000000..46b739c5 --- /dev/null +++ b/website/src/components/layout/Page/NotFoundWrapper.tsx @@ -0,0 +1,11 @@ +import styled from 'styled-components'; + +const NotFoundWrapper = styled('div')` + display: flex; + flex: 1 1 auto; + align-items: center; + justify-content: center; + padding: 0; +`; + +export default NotFoundWrapper; diff --git a/website/src/components/layout/Page/Page.tsx b/website/src/components/layout/Page/Page.tsx new file mode 100644 index 00000000..d3af01e0 --- /dev/null +++ b/website/src/components/layout/Page/Page.tsx @@ -0,0 +1,17 @@ +import styled from 'styled-components'; +import { dimensions } from 'components/foundations/variables'; + +interface PageProps { + docsPage?: boolean; +} + +const Page = styled('main')` + display: flex; + flex-direction: column; + flex: 1 1 auto; + position: relative; + margin-top: ${dimensions.heights.header}px; + padding: 0; +`; + +export default Page; diff --git a/website/src/components/layout/Page/index.ts b/website/src/components/layout/Page/index.ts new file mode 100644 index 00000000..aa342d76 --- /dev/null +++ b/website/src/components/layout/Page/index.ts @@ -0,0 +1,2 @@ +export { default as Page } from './Page'; +export { default as NotFoundWrapper } from './NotFoundWrapper'; diff --git a/website/src/components/page/Markdown/MarkdownComponents.tsx b/website/src/components/page/Markdown/MarkdownComponents.tsx new file mode 100644 index 00000000..5c4f4063 --- /dev/null +++ b/website/src/components/page/Markdown/MarkdownComponents.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import styled from 'styled-components'; +import { Heading, Paragraph } from 'components/foundations'; +import { space, textSizes, colors } from 'components/foundations/variables'; + +const UnorderedList = styled('ul')` + margin: ${space.sm}px 0; +`; + +export const h1 = (props: any) => ; +export const h2 = (props: any) => ; +export const h3 = (props: any) => ; +export const h4 = (props: any) => ; +export const h5 = (props: any) => ; +export const h6 = (props: any) => ; +export const p = (props: any) => ; +export const ul = (props: any) => ; +export const ol = (props: any) => ; +export const li = (props: any) => ; +export const table = styled('table')` + width: 100%; + margin: ${space.lg}px 0; + font-size: ${textSizes[400].fontSize}px; + line-height: ${textSizes[400].lineHeight}px; + border-collapse: collapse; + + thead { + border-bottom: 2px solid ${colors.grey02}; + + th { + padding: ${space.xs}px ${space.sm}px; + font-style: normal; + font-stretch: normal; + font-weight: 700; + letter-spacing: -0.01em; + text-transform: uppercase; + text-align: left; + color: ${colors.grey09}; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + } + } + + tfoot { + tr { + td { + padding: ${space.xs}px ${space.sm}px; + vertical-align: top; + font-style: normal; + font-stretch: normal; + font-weight: 700; + letter-spacing: -0.01em; + text-transform: uppercase; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + } + } + } + + tbody { + tr { + td { + padding: ${space.xs}px ${space.sm}px; + vertical-align: top; + font-size: ${textSizes[400].fontSize}px; + line-height: ${textSizes[400].lineHeight}px; + color: ${colors.grey07}; + + &:first-child { + padding-left: 0; + } + + &:last-child { + padding-right: 0; + } + } + } + } +`; diff --git a/website/src/components/page/Markdown/MarkdownContent.tsx b/website/src/components/page/Markdown/MarkdownContent.tsx new file mode 100644 index 00000000..20b09c5b --- /dev/null +++ b/website/src/components/page/Markdown/MarkdownContent.tsx @@ -0,0 +1,49 @@ +import React from 'react'; +import styled from 'styled-components'; +import { space, breakpoints } from 'components/foundations/variables'; + +interface MarkdownContentProps { + className?: string; + html?: string; +} + +const MarkdownContent: React.SFC = ({ className, html, children }) => { + if (html) { + return
; + } + + return
{children}
; +}; + +export default styled(MarkdownContent)` + .gatsby-highlight { + margin: ${space.sm}px 0; + } + + a[href^='#fn-'], + a[href^='#fnref-'] { + display: inline-block; + margin-left: 0.1rem; + font-weight: bold; + } + + .footnotes { + margin-top: 2rem; + font-size: 85%; + li[id^='fn-'] { + p { + // Remark for some reason puts the footnote reflink *after* the 'p' tag. + display: inline; + } + } + } + + .lead { + font-size: 1.25rem; + font-weight: 300; + + @media (min-width: ${breakpoints.md}) { + font-size: 1.5rem; + } + } +`; diff --git a/website/src/components/page/Markdown/index.ts b/website/src/components/page/Markdown/index.ts new file mode 100644 index 00000000..18d75c5d --- /dev/null +++ b/website/src/components/page/Markdown/index.ts @@ -0,0 +1 @@ +export { default as MarkdownContent } from './MarkdownContent'; diff --git a/website/src/components/ui/Pagination/Pagination.tsx b/website/src/components/ui/Pagination/Pagination.tsx new file mode 100644 index 00000000..34a03840 --- /dev/null +++ b/website/src/components/ui/Pagination/Pagination.tsx @@ -0,0 +1,224 @@ +import React from 'react'; +import styled from 'styled-components'; +import { Link } from 'gatsby'; +import { TocItem } from 'interfaces/nodes'; +import { Text, Heading } from 'components/foundations'; +import { space, breakpoints, colors } from 'components/foundations/variables'; + +const Wrapper = styled('aside')` + margin-bottom: ${space.xl}px; + border-collapse: collapse; + border-radius: 4px; + overflow: hidden; +`; + +const WrapperInner = styled('div')` + display: flex; + flex-direction: column; + + @media (min-width: ${breakpoints.md}px) { + flex-direction: row; + justify-content: space-between; + } +`; + +const PaginationButton = styled('div')` + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border-radius: 4px; + background-color: ${colors.grey02}; + + svg { + g { + fill: ${colors.grey07}; + } + } + + &:first-child { + margin-top: 16px; + margin-right: 24px; + } + + &:last-child { + margin-top: 16px; + margin-left: 24px; + } +`; + +const PaginationTitle = styled(Heading)``; + +const PaginationHeading = styled(Text)``; + +const PaginationLink = styled(Link)` + display: flex; + flex-direction: row; + align-items: center; + margin: 0; + + &:hover, + &:focus { + text-decoration: none; + } + + &:hover { + ${PaginationButton} { + background-color: ${colors.grey03}; + + svg { + g { + fill: ${colors.grey07}; + } + } + } + + ${PaginationTitle} { + color: ${colors.blue06}; + } + } + + &:focus { + ${PaginationButton} { + background-color: ${colors.blue07}; + + svg { + g { + fill: ${colors.white}; + } + } + } + + ${PaginationTitle} { + color: ${colors.grey07}; + } + } +`; + +const PaginationItem = styled('div')` + display: flex; + position: relative; + flex: 1 0 auto; + align-items: center; + height: 96px; + padding: 0 ${space.md}px; + overflow: hidden; + background-color: ${colors.grey01}; + border-radius: 4px; + + &:first-child { + justify-content: flex-start; + text-align: left; + } + + &:last-child { + margin-top: ${space.md}px; + justify-content: flex-end; + text-align: right; + } + + @media (min-width: ${breakpoints.md}px) { + flex: 1 0 50%; + + &:not(:first-child) { + border-left: none; + } + + &:first-child { + border-top-right-radius: 0; + border-bottom-right-radius: 0; + border-right: none; + } + + &:last-child { + margin-top: 0; + border-top-left-radius: 0; + border-bottom-left-radius: 0; + border-left: none; + } + } +`; + +const PaginationBlock = styled('div')``; + +interface PaginationProps { + prevPage?: TocItem; + nextPage?: TocItem; +} + +const Pagination: React.SFC = ({ prevPage, nextPage }) => ( + + + + {prevPage && ( + + + + + + + + + + + + + + + + + + + + Previous + + + {prevPage.title} + + + + )} + + + + {nextPage && ( + + + + Next + + + {nextPage.title} + + + + + + + + + + + + + + + + + + + + )} + + + +); + +export default Pagination; diff --git a/website/src/components/ui/Pagination/index.ts b/website/src/components/ui/Pagination/index.ts new file mode 100644 index 00000000..afd8de2b --- /dev/null +++ b/website/src/components/ui/Pagination/index.ts @@ -0,0 +1 @@ +export { default as Pagination } from './Pagination'; diff --git a/website/src/interfaces/gatsby.ts b/website/src/interfaces/gatsby.ts new file mode 100644 index 00000000..5e65fc80 --- /dev/null +++ b/website/src/interfaces/gatsby.ts @@ -0,0 +1,14 @@ +export interface SiteAuthor { + name: string; + url: string; + email: string; +} + +export interface SiteMetadata { + title: string; + sidebarTitle: string; + description: string; + siteUrl: string; + keywords: string; + author: SiteAuthor; +} diff --git a/website/src/interfaces/nodes.ts b/website/src/interfaces/nodes.ts new file mode 100644 index 00000000..7a5e6626 --- /dev/null +++ b/website/src/interfaces/nodes.ts @@ -0,0 +1,21 @@ +export interface HeaderMenuItem { + id: string; + label: string; + href: string; + external: boolean; +} + +export interface TocItem { + id: string; + slug: string; + title: string; +} + +export interface MenuNode { + title: string; + items: TocItem[]; +} + +export interface Edge { + node: T; +} diff --git a/website/src/layouts/index.tsx b/website/src/layouts/index.tsx new file mode 100644 index 00000000..76c7ac9b --- /dev/null +++ b/website/src/layouts/index.tsx @@ -0,0 +1,107 @@ +import React from 'react'; +import { Helmet } from 'react-helmet'; +import { graphql, StaticQuery } from 'gatsby'; +import { WindowLocation } from '@reach/router'; + +import { ThemeReset } from 'components/foundations'; +import { LayoutRoot } from 'components/layout/LayoutRoot'; +import { LayoutMain } from 'components/layout/LayoutMain'; +import { Navigation } from 'components/layout/Navigation'; +import { Overlay } from 'components/layout/Overlay'; + +import { MenuNode, Edge, HeaderMenuItem } from 'interfaces/nodes'; +import { SiteMetadata } from 'interfaces/gatsby'; + +interface IndexLayoutProps { + location?: WindowLocation; +} + +interface DataProps { + site: { + siteMetadata: SiteMetadata; + }; + navigationMenus: { + edges: Edge[]; + }; + headerMenus: { + edges: Edge[]; + }; +} + +const IndexLayout: React.FC = ({ location, children }) => { + return ( + + {(data: DataProps) => { + const { siteMetadata } = data.site; + + return ( + + + + {siteMetadata.title} + + + + + + + + + + + {children} + + + + ); + }} + + ); +}; + +export default IndexLayout; + +const query = graphql` + query IndexLayoutQuery { + site { + siteMetadata { + title + sidebarTitle + description + siteUrl + keywords + author { + name + url + email + } + } + } + navigationMenus: allTocJson { + edges { + node { + title + items { + id + slug + title + } + } + } + } + headerMenus: allMenuJson { + edges { + node { + id + label + href + external + } + } + } + } +`; diff --git a/website/src/pages/404.tsx b/website/src/pages/404.tsx new file mode 100644 index 00000000..eaae6fe2 --- /dev/null +++ b/website/src/pages/404.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import styled from 'styled-components'; +import { graphql, Link } from 'gatsby'; +import Helmet from 'react-helmet'; +import { RouteComponentProps } from '@reach/router'; + +import { Page, NotFoundWrapper } from 'components/layout/Page'; +import { SiteMetadata } from 'interfaces/gatsby'; +import { Heading, Text } from 'components/foundations'; +import IndexLayout from 'layouts'; + +interface Props extends RouteComponentProps { + data: { + site: { + siteMetadata: SiteMetadata; + }; + }; +} + +const NotFoundPage: React.SFC = ({ data }) => ( + + + + 404: Page not found. · {data.site.siteMetadata.title} + + + + + 404 + + + We can't find the page you're looking for. + + + Go back? + + + + + +); + +export default NotFoundPage; + +export const query = graphql` + query NotFoundPageQuery { + site { + siteMetadata { + title + description + siteUrl + keywords + author { + name + url + email + } + } + } + } +`; + +const Inner = styled('div')` + text-align: center; +`; diff --git a/website/src/templates/home.tsx b/website/src/templates/home.tsx new file mode 100644 index 00000000..f0cfc9c4 --- /dev/null +++ b/website/src/templates/home.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { graphql } from 'gatsby'; +import { Helmet } from 'react-helmet'; +import { RouteComponentProps } from '@reach/router'; + +import { Page } from 'components/layout/Page'; + +import { Container } from 'components/layout/Container'; +import { DocsWrapper } from 'components/docs/DocsWrapper'; +import { DocsHeader } from 'components/docs/DocsHeader'; +import MarkdownContent from 'components/page/Markdown/MarkdownContent'; + +import { MenuNode, Edge } from 'interfaces/nodes'; +import { Footer, FooterWrapper } from 'components/layout/Footer'; +import IndexLayout from 'layouts'; +import renderAst from 'utils/renderAst'; +// import FooterWrapper from 'components/old-layout/FooterWrapper'; +// import Footer from 'components/old-layout/Footer'; + +interface PageTemplateProps extends RouteComponentProps { + data: { + site: { + siteMetadata: { + title: string; + description: string; + author: { + name: string; + url: string; + }; + }; + }; + sectionList: { + edges: Edge[]; + }; + markdownRemark: { + htmlAst: any; + excerpt: string; + frontmatter: { + id: string; + title: string; + prev?: string; + next?: string; + }; + }; + }; +} + +const PageTemplate: React.SFC = ({ data }) => { + const { markdownRemark } = data; + + return ( + + + + + + + + + {renderAst(markdownRemark.htmlAst)} + +