aspect
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Write
        • Owners
        • Signed-in users
        • Everyone
        Owners Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights
    • Engagement control
    • Transfer ownership
    • Delete this note
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Versions and GitHub Sync Note Insights Sharing URL Help
Menu
Options
Engagement control Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Write
Owners
  • Owners
  • Signed-in users
  • Everyone
Owners Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       owned this note    owned this note      
    Published Linked with GitHub
    Subscribed
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    Subscribe
    --- title: rules_js tags: Bazel type: slide description: An excellent way to build JavaScript apps with Bazel slideOptions: theme: white --- <!-- .slide: data-background="https://hackmd.io/_uploads/S10D2xYY5.png" --> --- Slides: https://hackmd.io/@aspect/rules_js Note: This talk has a bunch of links which you can come find in the slides. --- Bazel: most scalable polyglot Build System. ![top-languages-over-the-years](https://hackmd.io/_uploads/H1y-RdzK9.png) JavaScript & TypeScript: most popular languages. Note: - Bazel works with all languages, let's make it good with the most popular ones. - image from https://octoverse.github.com/#top-languages-over-the-years --- :one: Introductions :two: Fetch and install npm packages :three: Runtime module resolutions :four: How to use rules_js Note: - I'll introduce Bazel and Node.js and explain where they clash, to motivate why we designed rules_js like we did. --- # :one: Introductions - Alex Eagle - Aspect Development - Bazel - NodeJS - pnpm - GH/bazelbuild/**rules_nodejs** ---- ## Who is Alex Eagle <img src="https://avatars0.githubusercontent.com/u/47395?s=460&v=4" style="border:2px solid grey; border-radius: 200px;" width=100/> - Worked at Google on DevInfra 2008-2020 - Bazel most of that time: TL for Google's CI, build/test results UI, Angular CLI - twitter.com/jakeherringbone ---- ## What is Aspect I Co-founded Aspect Development to make Bazel the industry-standard full-stack build system - <https://aspect.dev> - Support and consulting to help you adopt Bazel - <https://aspect.build> - Products making Bazel easier to use - <https://github.com/aspect-build> - rules_js is part of our Bazel rules ecosystem ---- ## What is Bazel - Build system for "every" langugage - Incremental: re-build proportional to what you changed - Cached/parallel: distribute over server farm - Scalable: works for Google's 2 billion line repo - Unix Philosophy: just spawns subprocesses, which can be any tool More: https://www.aspect.dev/resources Note: - Bazel is an "orchestrator" - This talk isn't the "bazel sales pitch" ---- ### When to consider Bazel for frontend - **Large-scale**: 1M SLOC / 100 devs - **Monorepo**: same use cases as Nx/Rush/Lerna - **Polyglot/full-stack**: parachute anywhere - **Integration testing**: fast test against backend - **Have a DevInfra team**: economy of scale More: https://www.aspect.dev/resources Note: - https://www.aspect.dev/blog/cboi-continuous-build-occasional-integration ---- ### None of those apply? Small, disconnected JS apps shouldn't use Bazel. The build system recommended by your framework is well supported for small-to-medium scale. ---- ## What is NodeJS JavaScript engine that runs outside the browser. Typically used for running dev tools to build and test JavaScript programs. Note: - just a runtime, requires a package manager ---- ## What is `pnpm` - "Fast, disk space efficient package manager": <https://pnpm.io/> - Works with nearly the whole ecosystem - Used by the <https://rushjs.io/> monorepo JS-only build tool - Happens to fit perfectly with Bazel semantics! Note: - Symlinks to a "virtual store" of fetched packages rather than nested layout - Their lockfile happens to present 100% of the info needed to fetch AND link the node_modules tree ---- ## What is `rules_nodejs` Bazel rules forked from Google-internal - toolchain to run hermetic NodeJS interpreter - shared Bazel interfaces ("Providers") like TypeScript `DeclarationInfo` rules_js is a layer on `rules_nodejs` `build_bazel_rules_nodejs` is replaced --- ## Build systems: ### Matrix / Hub-and-Spoke :construction: The JS ecosystem took a wrong turn - Grunt and Gulp fell out of favor - Instead, each tool became a Build System - Now each tool needs a plugin for each language ---- ![](https://hackmd.io/_uploads/BJWSKWKK5.png) ModernWeb Meetup: Layering in JS tooling https://www.aspect.dev/resources :point_right: last one Note: - I drew this live in my talk :laughing: - When each column is a tool like webpack or jest - each row is a language like typescript or sass - MxN support matrix - so many plugin authors to trust - A new framework like Qwik has to support every CSS preprocessor?? ---- ![](https://hackmd.io/_uploads/SJd_F-ttc.png) Like Gulp or Grunt, but way (way) better. Let's use Bazel! --- ## How Bazel works In five minutes :thinking_face: ---- ### Bazel: Loading Phase Load and evaluate all extensions, `BUILD` files and macros that are needed for the build. ```graphviz digraph { rankdir="LR" "sources" -> "dependency graph" "external repos" -> "dependency graph" } ``` Note: - bazel has to download stuff! ---- `bazel fetch [targets]` [![asciicast](https://asciinema.org/a/052YRxG2Pix7QsMk9dst1mTw0.svg)](https://asciinema.org/a/052YRxG2Pix7QsMk9dst1mTw0) ---- https://blog.aspect.dev/configuring-bazels-downloader - Bazel is full-featured for fetching external deps - Can air-gap, security scan, artifactory, etc - Supply-chain secure, Trust-on-first-use model - Cache based on integrity hashes ---- ### Bazel: Dependency Graph ```graphviz digraph { compound=true rankdir="LR" graph [ fontname="Source Sans Pro", fontsize=20 ]; node [ fontname="Source Sans Pro", fontsize=18]; edge [ fontname="Source Sans Pro", fontsize=12 ]; subgraph cluster1 { label="monorepo" my_app -> my_lib my_app -> my_comp } subgraph cluster2 { label="otherrepo" other_team_lib } subgraph cluster3 { label="registry.npmjs.com" request react express } my_lib -> request my_comp -> react my_app -> express my_app -> other_team_lib } ``` Note: - It's pretty much what you'd imagine as the dependency graph - Bazel can show you a graph like this for your workspace, try `bazel query` ---- `bazel query --output=graph [targets]` ```graphviz digraph mygraph { rankdir="LR" node [shape=box]; "//examples/macro:node_modules/mocha/dir" "//examples/macro:node_modules/mocha/dir" -> "//examples/macro:node_modules/mocha" "//examples/macro:node_modules" "//examples/macro:node_modules" -> "//examples/macro:node_modules/mocha" "//examples/macro:node_modules" -> "//examples/macro:node_modules/mocha-junit-reporter" "//examples/macro:node_modules" -> "//examples/macro:node_modules/mocha-multi-reporters" "//examples/macro:node_modules/mocha" "//examples/macro:node_modules/mocha-junit-reporter/dir" "//examples/macro:node_modules/mocha-junit-reporter/dir" -> "//examples/macro:node_modules/mocha-junit-reporter" "//examples/macro:test" "//examples/macro:test" -> "//examples/macro:_test_srcs\n//examples/macro:test__entry_point" "//examples/macro:test" -> "//examples/macro:node_modules/mocha-junit-reporter" "//examples/macro:test" -> "//examples/macro:node_modules/mocha-multi-reporters" "//examples/macro:node_modules/mocha-junit-reporter" "//examples/macro:node_modules/mocha-multi-reporters/dir" "//examples/macro:node_modules/mocha-multi-reporters/dir" -> "//examples/macro:node_modules/mocha-multi-reporters" "//examples/macro:node_modules/mocha-multi-reporters" "//examples/macro:_test_srcs\n//examples/macro:test__entry_point" } ``` ---- ### Bazel: Analysis Phase ```graphviz digraph { rankdir="LR" "dependency graph" -> "action graph" [label = "Bazel rules"] } ``` Action: for a requested output, how to generate it from some inputs and tools e.g. “if you need `hello.js`, run `swc` on `hello.ts`”. Requires predicting the outputs! ---- `bazel aquery [targets]` ```graphviz digraph { rankdir="LR" "index.ts" -> "index.js" [label=swc] "component.ts" -> "component.js" [label=tsc] "index.js" -> "bundle.js" [label=esbuild] "component.js" -> "bundle.js" "styles.scss" -> "styles.css" [label=sass] "styles.css" -> "serve" [label=express] "bundle.js" -> "serve" } ``` ---- ### Bazel: Execution Phase Execute a subset of the action graph by spawning subprocesses (e.g. `node`) ```graphviz digraph { rankdir="LR" "changed index.ts" [shape=note] "changed index.ts" -> "index.js" [label=swc] "component.ts" [shape=none] "component.js" [shape=none] "component.ts" -> "component.js" "index.js" -> "bundle.js" [label=esbuild] "component.js" -> "bundle.js" "styles.scss" [shape=none] "styles.scss" -> "styles.css" "styles.css" [shape=none] "styles.css" -> "serve" [label=express] "bundle.js" -> "serve" } ``` ---- User requested certain targets be built. Bazel is lazy and will only: - fetch precise dependencies needed - run actions required by the transitive dependency closure of those targets - run actions that are a "cache miss" --- # :two: Fetch and install npm packages ---- ### How npm/yarn solve it `npm install` Install everything needed for the whole package/workspace Any build/test script can depend on all npm packages :face_with_rolling_eyes: ---- ### How Google solves it Vendor the world: copy npm ecosystem sources into VCS - Never fetches from the internet - Never runs any package installation You *could* do it this way too. :face_with_one_eyebrow_raised: ---- ### How rules_nodejs solved it Just wrap `[npm|yarn] install` - install the world :thumbsdown: Guaranteed slow when repo rule invalidates :thumbsdown: Extra bad when "eager fetching" npm deps Note: - https://blog.aspect.dev/avoid-eager-fetches ---- ### rules_js: ideal solution :point_right: Port pnpm to Starlark :point_left: - re-use pnpm's resolver (via lockfile) - fetch with Bazel's downloader - unpack tarballs with Bazel - re-use `@pnpm/lifecycle` to run hooks - these are actions - can be remote cached - link `node_modules` Note: - More later about linking the node_modules ---- https://blog.aspect.dev/rulesjs-npm-benchmarks Best case: - BUILD file declares fine-grained deps - build only depends on one library - we only fetch/install one library! ---- ![](https://hackmd.io/_uploads/HkEK7nCF5.png) Note: - This is possible because BUILD files have finer-grained graph than package.json does ---- ## Workspaces Mix of third-party and first-party deps in a tree of package.json files. Google: single version policy rules_nodejs: independent top-level dep installs rules_js: supports pnpm workspaces! Note: - yarn, npm, pnpm all have this concept - one version policy is too hard to align all apps on the same versions - independent installs is hard to get transitive deps --- # :three: Resolving npm dependencies at runtime ---- ### How it works in npm NodeJS programs rely on a `node_modules` folder "Was a big mistake" says NodeJS creator, and Deno fixes it (but here we are :face_with_rolling_eyes:) The location of `node_modules` is expected to be relative to the location of the importing script. ---- ### How Google solves it: patch `require` Same strategy as "PnP", e.g. Yarn PnP. :thumbsdown: not compatible. Many npm packages wrote their own `require` implementation. ---- ### How rules_nodejs solves it: runtime "linker" Similar to `npm link`: use symlinks to make monorepo libraries appear in the node_modules tree :thumbsdown: Slow beginning of every NodeJS spawn :thumbsdown: Links appear in source tree w/o sandbox :thumbsdown: Bins don't work with `genrule`/`ctx.actions.run` :thumbsdown: Not compatible with "persistent workers" Note: - rules_nodejs also has the Google "patch require" strategy but is now discouraged. - doesn't make deps under the same tree with sources - 'rootDirs' troubles ---- ### How rules_js solves it :point_right: Linker is now just a standard Bazel target :point_left: Node.js tools assume the working dir is a single tree of src/gen/node_modules: we can do that! - "link" to `bazel-bin/node_modules/...` - copy sources to `bazel-bin` - actions first `cd bazel-out/[arch]/bin` Note: This requires `copy_to_bin` and a bit of care in custom rules. --- # :four: How to use rules_js Documentation and migration guide: https://docs.aspect.build/rules_js ---- ### Install Copy the `WORKSPACE` snippet from latest release. https://github.com/aspect-build/rules_js/releases ---- ### Adopt pnpm Just run `pnpm install` and check that your workflows work. > A few npm packages still have "hoisting bugs" where they don't declare correct dependencies and accidentally rely on npm or yarn-specific layout. Note: - there are good workarounds for broken packages ---- ### import `pnpm-lock.yaml` `npm_translate_lock` converts to Bazel's format (Starlark). `WORKSPACE` ```python= load("@aspect_rules_js//npm:npm_import.bzl", "npm_translate_lock") npm_translate_lock( name = "npm", pnpm_lock = "//:pnpm-lock.yaml", ) # Load the starlark version of the lockfile load("@npm//:repositories.bzl", "npm_repositories") npm_repositories() ``` Note: - Check in the result, or compute on-the-fly. - Can also import individual npm packages with no lockfile. - you can look in `$(bazel info output_base)/external/npm` to see what was generated ---- ### Link the npm packages `BUILD` (next to `package.json`) ```python= load("@npm//:defs.bzl", "js_link_all_packages") js_link_all_packages() ``` Result of `bazel build :all` is now ```bash= # the virtual store bazel-bin/node_modules/.aspect_rules_js # symlink into the virtual store bazel-bin/node_modules/some_pkg # If you used pnpm-workspace.yaml: bazel-bin/packages/some_pkg/node_modules/some_dep ``` ---- `bazel build examples/...` [![asciicast](https://asciinema.org/a/7W5JD1LUuvo84GmXtVVHLiqRi.svg)](https://asciinema.org/a/7W5JD1LUuvo84GmXtVVHLiqRi) ---- ### Link first-party packages First declare the package... `my-lib/BUILD` ```python= load("@aspect_rules_js//npm:defs.bzl", "npm_package") npm_package( name = "lib", srcs = [ "index.js", "package.json", ], ) ``` Note: - See rules_js/examples/lib ---- ### Link first-party packages ... then link to `bazel-bin/node_modules` tree... `app/BUILD` ```python= load("@aspect_rules_js//npm:defs.bzl", "npm_link_package") npm_link_package( name = "node_modules/@mycorp/mylib", src = "//examples/lib" ) ``` ---- ...then depend on it just like it came from npm! `app/BUILD` ```python= js_binary( name = "my_app", data = [ "//:node_modules/react-dom", "//:node_modules/@mycorp/mylib", ], entry_point = "index.js", ) ``` Note: - this is a desirable property, so that as you slurp in your manyrepos to the monorepo, the use sites don't change --- ## Running npm tools 1. Just call the `bin` entries from package.json 1. Write a macro wrapping a `bin` entry 1. Write a custom rule 1. Use an existing custom rule (e.g. rules_ts vs `tsc`) There are also more advanced ways, see rules_js/examples Note: These options are easy, hard, harder, easy ---- ### `bin` entries are provided for all packages `BUILD` ```python= load("@npm//typescript:package_json.bzl", typescript_bin = "bin") typescript_bin.tsc( name = "compile", srcs = [ "fs.ts", "tsconfig.json", "//:node_modules/@types/node", ], outs = ["fs.js"], chdir = package_name(), args = ["-p", "tsconfig.json"], ) ``` Note: - this doesn't cause an eager fetch! Bazel doesn't download the typescript package when loading this file ---- Each bin exposes three rules: | Use | With | To | | -------- | -------- | -------- | | `foo` | `bazel build` | produce outputs | | `foo_binary` | `bazel run` | side-effects | | `foo_test` | `bazel test` | assert exit `0` | ---- ### Wrap existing build system Use "component libraries" to get coarse granularity ```graphviz digraph { rankdir="LR" vite [label=vite] vite2 [label=vite] "lib sources" [shape=none] "lib sources" -> "vite" "app sources" [shape=none] "app sources" -> "vite2" vite -> vite2 vite2 -> bundle bundle [shape=none] } ``` ---- Pretty fast developer loop in <https://github.com/aspect-build/bazel-examples/tree/main/vue> ```python= load("@npm//vite:package_json.bzl", vite_bin = "bin") load("@npm//vue-tsc:package_json.bzl", vue_tsc_bin = "bin") vite_bin.vite( name = "dist", args = ["build"], out_dirs = ["dist"], srcs = [...], ) vue_tsc_bin.vue_tsc_test( name = "type-check", args = ["--noEmit"], data = [...], ) vite_bin.vite_binary( name = "vite", data = [...] ) ``` Note: - next we'll run this "vite" target ---- `ibazel run :vite` [![asciicast](https://asciinema.org/a/501915.svg)](https://asciinema.org/a/501915) ---- ### Write a Macro Bazel macros are like preprocessor definitions. Good way to give "syntax sugar", compose a few rules, set defaults. Indistinguishable from custom rules at use site Example: `mocha` test ---- ```python= def mocha_test(name, srcs, args = [], data = [], env = {}, **kwargs): bin.mocha_test( name = name, args = [ "--reporter", "mocha-multi-reporters", "--reporter-options", "configFile=$(location //examples/macro:mocha_reporters.json)", native.package_name() + "/*test.js", ] + args, data = data + srcs + [ "//examples/macro:mocha_reporters.json", "//examples/macro:node_modules/mocha-multi-reporters", "//examples/macro:node_modules/mocha-junit-reporter", ], env = dict(env, **{ # Add environment variable so that mocha writes its test xml # to the location Bazel expects. "MOCHA_FILE": "$$XML_OUTPUT_FILE", }), ) ``` [https://github.com/aspect-build/rules_js/blob/main/examples/macro/mocha.bzl](examples/macro) Note: - Expand a macro with `bazel query --output=build` ---- ### Write a custom rule Harder and not recommended for most users. Start from https://bazel.build/rules/rules-tutorial and use https://github.com/bazel-contrib/rules-template --- ### Use an existing custom rule From https://github.com/aspect-build: - rules_esbuild - Bazel rules for https://esbuild.github.io/ JS bundler - rules_terser - Bazel rules for https://terser.org/ - a JavaScript minifier - rules_swc - Bazel rules for the swc toolchain https://swc.rs/ - rules_ts - Bazel rules for the tsc compiler from http://typescriptlang.org ---- - rules_webpack - Bazel rules for webpack bundler https://webpack.js.org/ - rules_rollup - Bazel rules for https://rollupjs.org/ - a JavaScript bundler - rules_jest - Bazel rules to run tests using https://jestjs.io - rules_deno - Bazel rules for Deno http://deno.land ---- ... and many more by other vendors <http://docs.aspect.build> Catalog coming soon at https://bazel-contrib.github.io/SIG-rules-authors/ ---- ### Example custom rule: `ts_project` No more `rootDirs` in `tsconfig.json` :grin: --- `BUILD` ```python= load("@bazel_skylib//rules:write_file.bzl", "write_file") # Create a test fixture that is a non-trivial sized TypeScript program write_file( name = "gen_ts", out = "big.ts", content = [ "export const a{0}: number = {0}".format(x) for x in range(100000) ], ) ``` --- `BUILD` ```python= load("@aspect_rules_ts//ts:defs.bzl", "ts_project") ts_project( name = "tsc", srcs = ["big.ts"], declaration = True, source_map = True, ) ``` --- [ts_project with custom transpiler](https://github.com/aspect-build/bazel-examples/tree/main/ts_project_transpiler) `BUILD` ```python= load("@aspect_rules_swc//swc:defs.bzl", "swc_transpiler") ts_project( name = "swc", srcs = ["big.ts"], out_dir = "build-swc", transpiler = partial.make( swc_transpiler, args = ["--env-name=test"], swcrc = ".swcrc", ), ) ``` --- Benchmarks: ts_project w/ SWC https://blog.aspect.dev/rules-ts-benchmarks --- Transpile-only use case on large project `bazel build :devserver` ![](https://hackmd.io/_uploads/S1Y6E2AK5.png) Note: time for a first, non-incremental build --- ### Putting it all together Sophisticated teams can assemble their own toolchain. Create an entire JS build system just by composing existing tools in a macro! ![](https://hackmd.io/_uploads/SJd_F-ttc.png) Note: - "This is our moment": Build system authors can now build on top of Bazel and go back to composing tools --- Example: an entire custom build system called "differential loading": ```python= def differential_loading(name, entry_point, srcs): "Common workflow to serve TypeScript to modern browsers" ts_project( name = name + "_lib", srcs = srcs, ) rollup_bundle( name = name + "_chunks", deps = [name + "_lib"], sourcemap = "inline", config_file = "//:rollup.config.js", entry_points = { entry_point: "index", }, output_dir = True, ) # For older browsers, we'll transform the output chunks to es5 + systemjs loader bin.babel( name = name + "_chunks_es5", srcs = [ name + "_chunks", "es5.babelrc", ":node_modules/@babel/preset-env", ], output_dir = True, args = [ "../../../$(execpath %s_chunks)" % name, "--config-file", "../../../$(execpath es5.babelrc)", "--out-dir", "$(@D)", ], ) terser_minified( name = name + "_chunks_es5.min", src = name + "_chunks_es5", ) ``` --- # Roadmap rules_js 1.0.0 is available now Coming soon :tm: - Gazelle extension to generate `BUILD` files from srcs - Bazel 6.0 package manager: bzlmod instead of `WORKSPACE` https://blog.aspect.dev/bzlmod ---- # Thank you! These slides: https://hackmd.io/@aspect/rules_js Thanks conference organizers and everyone who helped launch rules_js. Come work with us on OSS! http://aspect.dev/careers Paid support and consulting: http://aspect.dev Our projects: github.com/aspect-build

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully