diff --git a/guide/source/deployment.md b/guide/source/deployment.md index fa9d8f08de..7c095350ee 100644 --- a/guide/source/deployment.md +++ b/guide/source/deployment.md @@ -196,6 +196,7 @@ MONGO_URL=mongodb://localhost:27017/myapp ROOT_URL=http://my-app.com PORT=3000 n * `ROOT_URL` is the base URL for your Meteor project * `PORT` is the port at which the application is running * `MONGO_URL` is a [Mongo connection string URI](https://docs.mongodb.com/manual/reference/connection-string/) supplied by the MongoDB provider. +* `METEOR_SETTINGS` is a JSON object containing your application settings (can also be set via --settings flag). **Warning:** Any settings under the `public` key will be sent to the client - never put secrets there. Unless you have a specific need to roll your own hosting environment, the other options here are definitely easier, and probably make for a better setup than doing everything from scratch. Operating a Meteor app in a way that it works correctly for everyone can be complex, and [Galaxy](#galaxy) handles a lot of the specifics like routing clients to the right containers and handling coordinated version updates for you. diff --git a/npm-packages/meteor-node-stubs/package-lock.json b/npm-packages/meteor-node-stubs/package-lock.json index 960afee14a..883dbf25ca 100644 --- a/npm-packages/meteor-node-stubs/package-lock.json +++ b/npm-packages/meteor-node-stubs/package-lock.json @@ -1,12 +1,12 @@ { "name": "meteor-node-stubs", - "version": "1.2.23", + "version": "1.2.26", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meteor-node-stubs", - "version": "1.2.23", + "version": "1.2.26", "bundleDependencies": [ "@meteorjs/crypto-browserify", "assert", @@ -171,9 +171,9 @@ } }, "node_modules/@meteorjs/create-ecdh/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -254,9 +254,9 @@ } }, "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -319,9 +319,9 @@ "license": "MIT" }, "node_modules/bn.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.2.tgz", - "integrity": "sha512-v2YAxEmKaBLahNwE1mjp4WON6huMNeuDvagFZW+ASCuA/ku0bXR9hSMw0XpiqMoA3+rmnyck/tPRSFQkoC9Cuw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz", + "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==", "inBundle": true, "license": "MIT" }, @@ -654,9 +654,9 @@ } }, "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -1184,9 +1184,9 @@ } }, "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -1459,9 +1459,9 @@ } }, "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.2.tgz", - "integrity": "sha512-n4DSx829VRTRByMRGdjQ9iqsN0Bh4OolPsFnaZBLcbi8iXcB+kJ9s7EnRt4wILZNV3kPLHkRVfOc/HvhC3ovDw==", + "version": "4.12.3", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz", + "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==", "inBundle": true, "license": "MIT" }, @@ -1473,9 +1473,9 @@ "license": "MIT" }, "node_modules/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "inBundle": true, "license": "BSD-3-Clause", "dependencies": { diff --git a/npm-packages/meteor-node-stubs/package.json b/npm-packages/meteor-node-stubs/package.json index 3e2867429e..8dbeb95104 100644 --- a/npm-packages/meteor-node-stubs/package.json +++ b/npm-packages/meteor-node-stubs/package.json @@ -2,7 +2,7 @@ "name": "meteor-node-stubs", "author": "Ben Newman ", "description": "Stub implementations of Node built-in modules, a la Browserify", - "version": "1.2.24", + "version": "1.2.26", "main": "index.js", "license": "MIT", "homepage": "https://github.com/meteor/meteor/blob/devel/npm-packages/meteor-node-stubs/README.md", diff --git a/packages/accounts-base/accounts-base.d.ts b/packages/accounts-base/accounts-base.d.ts index 14451732b5..73e26ae913 100644 --- a/packages/accounts-base/accounts-base.d.ts +++ b/packages/accounts-base/accounts-base.d.ts @@ -3,11 +3,20 @@ import { Meteor } from 'meteor/meteor'; import { Configuration } from 'meteor/service-configuration'; import { DDP } from 'meteor/ddp'; +/** + * Object containing functions that generate URLs for account-related emails. + * Override these to customize URLs in password reset, enrollment, and verification emails. + * URL methods can return either a string or a Promise that resolves to a string. + */ export interface URLS { - resetPassword: (token: string) => string; - verifyEmail: (token: string) => string; - loginToken: (token: string) => string; - enrollAccount: (token: string) => string; + /** Generates the URL for password reset emails. Can return a Promise for async URL generation. */ + resetPassword: (token: string, extraParams?: Record) => string | Promise; + /** Generates the URL for email verification emails. Can return a Promise for async URL generation. */ + verifyEmail: (token: string, extraParams?: Record) => string | Promise; + /** Generates the URL for login token emails. Can return a Promise for async URL generation. */ + loginToken: (selector: string, token: string, extraParams?: Record) => string | Promise; + /** Generates the URL for account enrollment emails. Can return a Promise for async URL generation. */ + enrollAccount: (token: string, extraParams?: Record) => string | Promise; } export interface EmailFields { diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 4d8a0c3d5f..cd4e816460 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -83,6 +83,25 @@ export class AccountsServer extends AccountsCommon { return Meteor._isPromise(value) ? await value : value; }; + /** + * @summary Object containing functions that generate URLs for account-related emails. + * Override these to customize URLs in emails sent by + * [`Accounts.sendResetPasswordEmail`](#Accounts-sendResetPasswordEmail), + * [`Accounts.sendEnrollmentEmail`](#Accounts-sendEnrollmentEmail), and + * [`Accounts.sendVerificationEmail`](#Accounts-sendVerificationEmail). + * + * By default, URLs use hash fragments (e.g., `#/reset-password/:token`) for security: + * hash fragments are not sent to the server in HTTP requests, preventing tokens from + * appearing in server logs or referrer headers. + * @locus Server + * @memberof Accounts + * @name urls + * @type {Object} + * @property {Function} resetPassword - `(token, extraParams) => string` - Generates password reset URL. + * @property {Function} verifyEmail - `(token, extraParams) => string` - Generates email verification URL. + * @property {Function} enrollAccount - `(token, extraParams) => string` - Generates account enrollment URL. + * @property {Function} loginToken - `(selector, token, extraParams) => string` - Generates login token URL. + */ this.urls = { resetPassword: (token, extraParams) => this.buildEmailUrl(`#/reset-password/${token}`, extraParams), verifyEmail: (token, extraParams) => this.buildEmailUrl(`#/verify-email/${token}`, extraParams), @@ -93,6 +112,16 @@ export class AccountsServer extends AccountsCommon { this.addDefaultRateLimit(); + /** + * @summary Builds a URL for account-related emails by combining the app's + * root URL with a path and optional extra parameters. + * @locus Server + * @memberof Accounts + * @name buildEmailUrl + * @param {String} path - The path to append to the root URL (e.g., `#/reset-password/TOKEN`). + * @param {Object} [extraParams={}] - Additional query parameters to include in the URL. + * @returns {String} The complete URL. + */ this.buildEmailUrl = (path, extraParams = {}) => { const url = new URL(Meteor.absoluteUrl(path)); const params = Object.entries(extraParams); diff --git a/tools/isobuild/bundler.js b/tools/isobuild/bundler.js index f64a061476..855d7ce2b0 100644 --- a/tools/isobuild/bundler.js +++ b/tools/isobuild/bundler.js @@ -3133,6 +3133,7 @@ Node.js ${process.version}. To run the application: $ export MONGO_URL='mongodb://user:password@host:port/databasename' $ export ROOT_URL='http://example.com' $ export MAIL_URL='smtp://user:password@mailhost:port/' + $ export METEOR_SETTINGS='{"public":{"key":"value"}}' $ node main.js Use the PORT environment variable to set the port where the diff --git a/v3-docs/docs/.vitepress/config.mts b/v3-docs/docs/.vitepress/config.mts index 09f93cf97f..bef4db33ed 100644 --- a/v3-docs/docs/.vitepress/config.mts +++ b/v3-docs/docs/.vitepress/config.mts @@ -413,10 +413,26 @@ export default defineConfig({ }, ] }, + { + text: "Using Atmosphere packages", + link: "/packages/6.using-atmosphere-packages", + }, + { + text: "Writing Atmosphere packages", + link: "/packages/7.writing-atmosphere-packages", + }, { link: "/packages/packages-listing", text: "Maintained Packages", }, + { + text: "Using npm packages", + link: "/packages/4.using-npm-packages", + }, + { + text: "Writing npm packages", + link: "/packages/5.writing-npm-packages", + }, { link: "/community-packages/index", text: "Community Packages", @@ -467,6 +483,10 @@ export default defineConfig({ text: "MongoDB Connection", link: "/troubleshooting/mongodb-connection", }, + { + text: "Hot Code Push", + link: "/troubleshooting/hot-code-push", + }, ], collapsed: true, }, @@ -506,13 +526,76 @@ export default defineConfig({ link: "/tutorials/application-structure/index", text: "Application structure", }, + { + text: "Build System", + link: "/about/build-tool", + }, + { + text: "Core Concepts", + items: [ + { + text: "Methods", + link: "/tutorials/methods/methods", + }, + { + text: "Data Loading", + link: "/tutorials/data-loading/data-loading", + }, + { + text: "Collections & Schemas", + link: "/tutorials/collections/collections", + }, + { + text: "Accounts", + link: "/tutorials/accounts/accounts", + }, + { + text: "Routing", + link: "/tutorials/routing/routing", + }, + ] + }, { text: "Production", items:[ { text: "Security", link: "/tutorials/security/security", - } + }, + { + text: "Testing", + link: "/tutorials/testing/testing", + }, + { + text: "Deployment", + link: "/tutorials/deployment/deployment", + }, + ] + }, + { + text: "Advanced Topics", + items: [ + { + text: "Apollo & GraphQL", + link: "/tutorials/apollo/apollo", + }, + { + text: "Code Style", + link: "/tutorials/code-style/code-style", + }, + ] + }, + { + text: "Integrations", + items: [ + { + text: "React Native", + link: "/tutorials/integrations/react-native", + }, + { + text: "Flowbite UI", + link: "/tutorials/integrations/flowbite", + }, ] }, ], @@ -537,11 +620,16 @@ export default defineConfig({ { text: "Performance", items: [ + { + text: "Performance Improvements", + link: "/performance/performance-improvement", + }, { text: "WebSocket Compression", link: "/performance/websocket-compression", }, ], + collapsed: true, }, ], diff --git a/v3-docs/docs/about/build-tool.md b/v3-docs/docs/about/build-tool.md new file mode 100644 index 0000000000..752a3a1164 --- /dev/null +++ b/v3-docs/docs/about/build-tool.md @@ -0,0 +1,388 @@ +# Build System + +This guide covers how Meteor's build system compiles your app. + +After reading this guide, you'll know: + +1. What the Meteor build tool does +2. How to choose between Meteor Bundler and Rspack integration +3. How JavaScript transpilation works +4. How to configure CSS processing +5. How to use Hot Module Replacement +6. How to write custom build plugins + +## What does it do? + +The Meteor build system is the actual command line tool that you get when you install Meteor. You run it by typing the `meteor` command in your terminal, possibly followed by a set of arguments. Read the [CLI documentation](/cli/) or type `meteor help` in your terminal to learn about all of the commands. + +The Meteor build tool is what compiles, runs, deploys, and publishes all of your Meteor apps and packages. It's Meteor's built-in solution to the problems also solved by tools like Grunt, Gulp, Webpack, Browserify, Nodemon, and many others. + +Starting with Meteor 3.3, you can use the **Modern Build Stack** which includes optimizations with SWC transpilation. With Meteor 3.4, you can integrate **Rspack** as your application bundler for even faster builds, smaller bundle sizes, and modern bundling features. See the [Modern Build Stack overview](/about/modern-build-stack) for details. + +### Reloads app on file change + +After executing the `meteor` command to start the build tool you should leave it running while further developing your app. The build tool automatically detects any relevant file changes using a file watching system and recompiles the necessary changes, restarting your client or server environment as needed. [Hot module replacement](#hot-module-replacement) can optionally be used so you can view and test your changes even quicker. + +### Compiles files with build plugins + +The main function of the Meteor build tool is to run "build plugins". These plugins define different parts of your app build process. Meteor puts heavy emphasis on reducing or removing build configuration files, so you won't see any large build process config files like you would in Gulp or Webpack. The Meteor build process is configured almost entirely through adding and removing packages to your app and putting files in specially named directories. + +For example, to get all of the newest stable ES2015 JavaScript features in your app, you add the [`ecmascript` package](/packages/ecmascript). This package provides support for ES2015 modules, which gives you even more fine-grained control over file load order using ES2015 `import` and `export`. As new Meteor releases add new features to this package you get them for free. + +### Controlling which files to build + +By default Meteor will build certain files as controlled by your application [file structure](/tutorials/application-structure/) and Meteor's default file load order rules. However, you may override the default behavior using `.meteorignore` files, which cause the build system to ignore certain files and directories using the same pattern syntax as `.gitignore` files. These files may appear in any directory of your app or package, specifying rules for the directory tree below them. These `.meteorignore` files are also fully integrated with Meteor's file watching system, so they can be added, removed, or modified during development. + +### Combines and minifies code + +Another important feature of the Meteor build tool is that it automatically concatenates your application asset files, and in production minifies these bundles. This lets you add all of the comments and whitespace you want to your source code and split your code into as many files as necessary, all without worrying about app performance and load times. + +By default, this is enabled by the [`standard-minifier-js`](https://atmospherejs.com/meteor/standard-minifiers-js) and [`standard-minifier-css`](/packages/standard-minifier-css) packages, which use Terser for JavaScript minification. + +**Using SWC Minifier (Meteor 3.3+)**: When you enable the Modern Build Stack with `"modern": true`, Meteor automatically uses the SWC minifier, which is significantly faster than Terser while producing similar or smaller bundle sizes. + +**Using Rspack (Meteor 3.4+)**: When using the Rspack integration, minification is handled by Rspack's built-in SWC minifier, offering the fastest minification with advanced optimizations. + +If you need different minification behavior, you can replace these packages (see [zodern:standard-minifier-js](https://atmospherejs.com/zodern/standard-minifier-js) as an example). + +### Development vs. production + +Running an app in development is all about fast iteration time. All kinds of different parts of your app are handled differently and instrumented to enable better reloads and debugging. In production, the app is reduced to the necessary code and functions just like any standard Node.js app. + +Therefore, you shouldn't run your app in production by executing the `meteor run` command. Instead, follow the directions in [Deploying Meteor Applications](/tutorials/deployment/deployment). If you find an error in production that you suspect is related to minification, you can run the minified version of your app locally for testing with `meteor --production`. + +## Modern Build Stack (Meteor 3.3+) + +Meteor 3.3 introduced the Modern Build Stack, a series of optimizations to make your builds faster and more efficient. Meteor 3.4 expanded this with Rspack integration for even greater performance. + +### Meteor Bundler Optimizations (3.3+) + +The optimized Meteor bundler includes: + +- **SWC Transpilation**: Replace Babel with the faster SWC transpiler for JavaScript/TypeScript compilation +- **SWC Minification**: Use SWC minifier instead of Terser for faster production builds +- **Modern-only Development**: Skip legacy browser builds during development +- **Fast File Watching**: Uses @parcel/watcher for native recursive file watching +- **Enhanced .meteorignore**: Better control over which files are built + +To enable these optimizations, add to your `package.json`: + +```json +{ + "meteor": { + "modern": true + } +} +``` + +Learn more in the [Meteor Bundler Optimizations guide](/about/modern-build-stack/meteor-bundler-optimizations). + +### Rspack Bundler Integration (3.4+) + +Meteor 3.4 introduces optional Rspack integration as a modern, high-performance alternative to the traditional Meteor bundler. Rspack offers significantly faster builds (~5-10x), smaller bundle sizes (20-40% reduction), and access to the modern bundler ecosystem. + +**Quick Start:** + +```bash +# Add the rspack package (included by default in new apps) +meteor add rspack +``` + +**For complete details on Rspack features, configuration, migration guides, and framework integrations, see the [Rspack Bundler Integration guide](/about/modern-build-stack/rspack-bundler-integration).** + +### Comparing Build Options + +| Feature | Meteor Bundler | Meteor + Optimizations | Meteor + Rspack | +|---------|---------------|----------------------|-----------------| +| **Setup** | Default | Add `"modern": true` | Add `rspack` package | +| **Transpiler** | Babel | SWC (with Babel fallback) | SWC via Rspack | +| **Minifier** | Terser | SWC | SWC via Rspack | +| **Build Speed** | Baseline | ~2-3x faster | ~5-10x faster | +| **Bundle Size** | Baseline | Similar | 20-40% smaller | +| **HMR Speed** | Standard | Faster | Fastest | +| **Code Splitting** | Limited | Limited | Full HTTP/2 support | +| **Tree Shaking** | Basic | Basic | Advanced | +| **Ecosystem** | Atmosphere | Atmosphere | Rspack + Atmosphere | +| **Entry Points** | Optional | Optional | Required | +| **Migration** | N/A | Minimal | Moderate | + +## JavaScript transpilation + +These days, the landscape of JavaScript tools and frameworks is constantly shifting, and the language itself is evolving just as rapidly. It's no longer reasonable to wait for web browsers to implement the language features you want to use. Most JavaScript development workflows rely on compiling code to work on the lowest common denominator of environments, while letting you use the newest features in development. Meteor has support for some of the most popular tools out of the box. + +### ES2015+ (recommended) + +The `ecmascript` package (which is installed into all new apps and packages by default, but can be removed), allows support for many ES2015+ features. We recommend using it. You can read more about it in the [Code Style](/tutorials/code-style/code-style) article. + +### SWC (Meteor 3.3+, recommended for performance) + +Starting with Meteor 3.3, you can use SWC as your transpiler for significantly faster builds. SWC is a Rust-based JavaScript/TypeScript compiler that's 20-70x faster than Babel while supporting the same features. + +To enable SWC, add to your `package.json`: + +```json +{ + "meteor": { + "modern": true + } +} +``` + +SWC will handle all transpilation with automatic fallback to Babel for any incompatible code. Learn more in the [Meteor Bundler Optimizations guide](/about/modern-build-stack/meteor-bundler-optimizations). + +### Babel + +Babel is a mature, configurable transpiler which allows you to write code in the latest version of JavaScript even when your supported environments don't support certain features natively. Babel will compile those features down to a supported version. + +Meteor provides a set of appropriate core plugins for each environment (Node.js, modern browsers, and legacy browsers) and React to support most modern JavaScript code practices. In addition, Meteor supports custom `.babelrc` files which allows developers to further customize their Babel configuration to suit their needs (e.g., Stage 0 proposals). + +Developers are encouraged to avoid adding large presets (such as babel-preset-env and babel-preset-react) and instead add specific plugins as needed. You will avoid unnecessary Babel compilation and you'll be less likely to experience plugin ordering issues. + +> **Note**: When using the Modern Build Stack with `"modern": true`, Babel is used as a fallback for any code that SWC cannot handle, ensuring compatibility with all existing Meteor code. + +### CoffeeScript + +While we recommend using ES2015 with the `ecmascript` package as the best development experience for Meteor, everything in the platform is 100% compatible with [CoffeeScript](http://coffeescript.org/) and many people in the Meteor community prefer it. + +All you need to do to use CoffeeScript is add the right Meteor package: + +```bash +meteor add coffeescript +``` + +All code written in CoffeeScript compiles to JavaScript under the hood, and is completely compatible with any code in other packages that is written in JS or ES2015. + +> **Recommended (3.4+)**: If you're using CoffeeScript, consider using it with the [Rspack bundler](/about/modern-build-stack/rspack-bundler-integration#coffeescript) instead of the Atmosphere build plugin. Rspack handles CoffeeScript via `coffee-loader` and `swc-loader`, providing faster builds and better integration with the modern build stack. You can quickly scaffold a new project with `meteor create --coffeescript`. + +### TypeScript + +[TypeScript](https://www.typescriptlang.org/) is modern JavaScript with optional types and more. Adding types will make your code more readable and less prone to runtime errors. + +TypeScript can be installed with: + +```bash +meteor add typescript +``` + +It is necessary to configure the TypeScript compiler with a `tsconfig.json` file. Here's the one generated by `meteor create --typescript`: + +```json +{ + "compilerOptions": { + "target": "es2018", + "module": "esNext", + "lib": ["esnext", "dom"], + "allowJs": true, + "checkJs": false, + "jsx": "preserve", + "incremental": true, + "noEmit": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": false, + "noFallthroughCasesInSwitch": false, + "baseUrl": ".", + "paths": { + "/*": ["*"] + }, + "moduleResolution": "node", + "resolveJsonModule": true, + "types": ["node", "mocha"], + "esModuleInterop": true, + "preserveSymlinks": true + }, + "exclude": [ + "./.meteor/**", + "./packages/**" + ] +} +``` + +If you want to add TypeScript from the point of project creation, you can run the create command with the --typescript flag: + +```bash +meteor create --typescript name-of-my-new-typescript-app +``` + +#### Conditional imports + +TypeScript does not support nested `import` statements, therefore conditionally importing modules requires you to use the `require` statement. + +To maintain type safety, you can take advantage of TypeScript's import elision and reference the types using the `typeof` keyword. See the [TypeScript handbook article](https://www.typescriptlang.org/docs/handbook/modules.html#optional-module-loading-and-other-advanced-loading-scenarios) for details. + +## Templates and HTML + +Since Meteor uses client-side rendering for your app's UI, all of your HTML code, UI components, and templates need to be compiled to JavaScript. There are a few options at your disposal to write your UI code. + +### Blaze HTML templates + +The aptly named `blaze-html-templates` package that comes with every new Meteor app by default compiles your `.html` files written using [Spacebars](http://blazejs.org/api/spacebars.html) into Blaze-compatible JavaScript code. You can also add `blaze-html-templates` to any of your packages to compile template files located in the package. + +[Read about how to use Blaze and Spacebars in the Blaze documentation.](http://blazejs.org/guide/spacebars.html) + +### Blaze Jade templates + +If you don't like the Spacebars syntax Meteor uses by default and want something more concise, you can give Jade a try by using [`pacreach:jade`](https://atmospherejs.com/pacreach/jade). This package will compile all files in your app with the `.jade` extension into Blaze-compatible code, and can be used side-by-side with `blaze-html-templates` if you want to have some of your code in Spacebars and some in Jade. + +### JSX for React + +If you're building your app's UI with React, currently the most popular way to write your UI components involves JSX, an extension to JavaScript that allows you to type HTML tags that are converted to React DOM elements. JSX code is handled automatically by the `ecmascript` package. + +## CSS processing + +> **Using Rspack (3.4+)?** When using the Rspack bundler, CSS is handled by Rspack's built-in loaders instead of Meteor's Atmosphere build plugins. This gives you standard bundler conventions, proper CSS HMR, and access to tools like Tailwind and PostCSS without extra Meteor packages. See the [Rspack CSS guide](/about/modern-build-stack/rspack-bundler-integration#css) for details. + +All your CSS style files will be processed using Meteor's default file load order rules along with any import statements and concatenated into a single stylesheet, `merged-stylesheets.css`. In a production build this file is also minified. By default this single stylesheet is injected at the beginning of the HTML `` section of your application. + +However, this can potentially be an issue for some applications that use a third party UI framework, such as Bootstrap, which is loaded from a CDN. This could cause Bootstrap's CSS to come after your CSS and override your user-defined styles. + +To get around this problem Meteor supports the use of a pseudo tag `` that if placed anywhere in the `` section your app will be replaced by a link to this concatenated CSS file. If this pseudo tag isn't used, the CSS file will be placed at the beginning of the `` section as before. + +### CSS pre-processors + +It's no secret that writing plain CSS can often be a hassle as there's no way to share common CSS code between different selectors or have a consistent color scheme between different elements. CSS compilers, or pre-processors, solve these issues by adding extra features on top of the CSS language like variables, mixins, math, and more, and in some cases also significantly change the syntax of CSS to be easier to read and write. + +Here are three example CSS pre-processors supported by Meteor: + +1. [Sass](http://sass-lang.com/) +2. [Less.js](http://lesscss.org/) +3. [Stylus](https://learnboost.github.io/stylus/) + +They all have their pros and cons, and different people have different preferences. Sass with the SCSS syntax is quite popular as CSS frameworks like Bootstrap have switched to Sass, and the C++ LibSass implementation appears to be faster than some of the other compilers available. + +CSS framework compatibility should be a primary concern when picking a pre-processor, because a framework written with Less won't be compatible with one written in Sass. + +### Source vs. import files + +An important feature shared by all of the available CSS pre-processors is the ability to import files. This lets you split your CSS into smaller pieces, and provides a lot of the same benefits that you get from JavaScript modules: + +1. You can control the load order of files by encoding dependencies through imports, since the load order of CSS matters. +2. You can create reusable CSS "modules" that only have variables and mixins and don't actually generate any CSS. + +In Meteor, each of your `.scss`, `.less`, or `.styl` source files will be one of two types: "source" or "import". + +A "source" file is evaluated eagerly and adds its compiled form to the CSS of the app immediately. + +An "import" file is evaluated only if imported from some other file and can be used to share common mixins and variables between different CSS files in your app. + +Read the documentation for each package listed below to see how to indicate which files are source files vs. imports. + +### Importing styles + +In all three Meteor supported CSS pre-processors you can import other style files from both relative and absolute paths in your app and from both npm and Meteor Atmosphere packages. + +```less +@import '../stylesheets/colors.less'; // a relative path +@import '{}/imports/ui/stylesheets/button.less'; // absolute path with `{}` syntax +``` + +You can also import CSS from a JavaScript file if you have the `ecmascript` package installed: + +```js +import '../stylesheets/styles.css'; +``` + +> When importing CSS from a JavaScript file, that CSS is not bundled with the rest of the CSS processed with the Meteor build tool, but instead is put in your app's `` tag inside `` after the main concatenated CSS file. + +Importing styles from an Atmosphere package using the `{}` package name syntax: + +```less +@import '{my-package:pretty-buttons}/buttons/styles.import.less'; +``` + +> CSS files in an Atmosphere package are declared with `api.addFiles`, and therefore will be eagerly evaluated, and automatically bundled with all the other CSS in your app. + +Importing styles from an npm package using the `{}` syntax: + +```less +@import '{}/node_modules/npm-package-name/button.less'; +``` + +```js +import 'npm-package-name/stylesheets/styles.css'; +``` + +### Sass + +The best Sass build plugin for Meteor is [`leonardoventurini:scss`](https://atmospherejs.com/leonardoventurini/scss). An alternative to the previous recommended [`fourseven:scss`](https://atmospherejs.com/fourseven/scss) package. + +With Rspack (3.4+), you can replace the Atmosphere package with `sass-embedded` and `sass-loader` configured in your `rspack.config.js`. See the [Rspack CSS guide](/about/modern-build-stack/rspack-bundler-integration#css) for setup instructions. + +### Less + +Less is maintained as a [Meteor core package called `less`](/packages/less). + +With Rspack (3.4+), you can replace the Atmosphere package with `less` and `less-loader` configured in your `rspack.config.js`. See the [Rspack CSS guide](/about/modern-build-stack/rspack-bundler-integration#css) for setup instructions. + +### Stylus + +The best Stylus build plugin for Meteor is [coagmano:stylus](https://atmospherejs.com/coagmano/stylus). + +## PostCSS and Autoprefixer + +In addition to CSS pre-processors like Sass, Less, and Stylus, there is now an ecosystem of CSS post-processors. Regardless of which CSS pre-processor you use, a post-processor can give you additional benefits like cross-browser compatibility. + +The most popular CSS post-processor right now is [PostCSS](https://github.com/postcss/postcss), which supports a variety of plugins. [Autoprefixer](https://github.com/postcss/autoprefixer) is perhaps the most useful plugin, since it enables you to stop worrying about browser prefixes and compatibility and write standards-compliant CSS. No more copying 5 different statements every time you want a CSS gradient - you can write a standard gradient without any prefixes and Autoprefixer handles it for you. + +Meteor automatically runs PostCSS for you once you've configured it. Learn more about enabling it in the docs for [standard-minifier-css](/packages/standard-minifier-css). + +## Hot Module Replacement + +In Meteor apps, JavaScript, TypeScript, CSS files that are dynamically imported, and many other types of files are converted into JavaScript modules during the build process. Instead of reloading the client after a rebuild, Meteor is able to update the JavaScript modules within the running application that were modified. This reduces the feedback cycle while developing by allowing you to view and test your changes quicker. + +### Meteor HMR + +Hot module replacement (HMR) can be enabled by adding the [hot-module-replacement](/packages/hot-module-replacement) package to your app: + +```bash +meteor add hot-module-replacement +``` + +Many types of JavaScript modules cannot be updated with HMR, so HMR has to be configured to know which modules can be replaced and how to replace them. Most apps never need to do this manually. Instead, you can use integrations that configure HMR for you: + +- React components are automatically updated using [React Fast Refresh](https://atmospherejs.com/meteor/react-fast-refresh). This integration is enabled for all Meteor apps that use HMR and a supported React version. +- Svelte files can be automatically updated with HMR by using the [zodern:melte](https://atmospherejs.com/zodern/melte) compiler package. +- Vue components can be updated with HMR using [akryum:vue-component](https://atmospherejs.com/akryum/vue-component). +- Some packages are able to help automatically dispose old versions of modules. For example, [zodern:pure-admin](https://atmospherejs.com/zodern/pure-admin) removes menu items and pages added in the old version of the module. + +To further control how HMR applies updates in your app, you can use the [hot API](/packages/hot-module-replacement). This can be used to accept updates for additional types of files, help dispose a module so the old version no longer affects the app (such as stopping Tracker.autorun computations), or creating your own integrations with other view layers or libraries. + +If a change was made to the app that cannot be applied with HMR, it reloads the page with hot code push, as is done when HMR is not enabled. It currently only supports app code in the modern client architecture. + +### Rspack HMR (3.4+) + +When using the [Rspack integration](/about/modern-build-stack/rspack-bundler-integration), you get Rspack's native HMR, which is significantly faster than Meteor's traditional HMR: + +- **Faster Updates**: Changes are reflected almost instantly +- **Persistent State**: Better preservation of application state during updates +- **Framework Integration**: Automatic support for React Fast Refresh, Vue HMR, Svelte HMR, and more +- **CSS HMR**: Style changes apply without full page reload (unlike traditional Cordova apps) + +**Note**: Blaze HMR is not currently supported with Rspack. Blaze apps will still reload quickly due to Rspack's fast rebuild times (97% reduction), but will perform a full page reload instead of hot module replacement. + +To use Rspack HMR, simply add the `rspack` package - HMR is enabled automatically for supported frameworks. + +## Build plugins + +The most powerful feature of Meteor's build system is the ability to define custom build plugins. If you find yourself writing scripts that mangle one type of file into another, merge multiple files, or something else, it's likely that these scripts would be better implemented as a build plugin. The `ecmascript`, `templating`, and `coffeescript` packages are all implemented as build plugins, so you can replace them with your own versions if you want to! + +[Read the documentation about build plugins.](/api/package#build-plugin-api) + +### Types of build plugins + +There are three types of build plugins supported by Meteor today: + +1. **Compiler plugin** - compiles source files (LESS, CoffeeScript) into built output (JS, CSS, asset files, and HTML). Only one compiler plugin can handle a single file extension. +2. **Minifier plugin** - compiles lots of built CSS or JS files into one or more minified files, for example `standard-minifiers`. Only one minifier can handle each of `js` and `css`. +3. **Linter plugin** - processes any number of files, and can print lint errors. Multiple linters can process the same files. + +### Writing your own build plugin + +Writing a build plugin is a very advanced task that only the most advanced Meteor users should get into. The best place to start is to copy a different plugin that is the most similar to what you are trying to do. For example, if you wanted to make a new CSS compiler plugin, you could fork the `less` package; if you wanted to make your own JS transpiler, you could fork `ecmascript`. A good example of a linter is the `jshint` package, and for a minifier you can look at `standard-minifiers-js` and `standard-minifiers-css`. + +### Caching + +The best way to make your build plugin fast is to use caching anywhere you can - the best way to save time is to do less work! Check out the [documentation about CachingCompiler](/api/package#caching-compiler) to learn more. It's used in all of the above examples, so you can see how to use it by looking at them. diff --git a/v3-docs/docs/about/cordova.md b/v3-docs/docs/about/cordova.md index 14be2db4cf..621898dce7 100644 --- a/v3-docs/docs/about/cordova.md +++ b/v3-docs/docs/about/cordova.md @@ -9,7 +9,7 @@ Meteor allows developers to build mobile applications using web technologies lik Cordova apps run in a web view, which is like a browser without the UI. Different browser engines have varying implementations and support for web standards. This means the web view your app uses can greatly affect its performance and available features. (For details on supported features across browsers and versions, check caniuse.com.) -There is a [Meteor Cordova guide](https://guide.meteor.com/cordova) available that offers advanced configuration details for Meteor Cordova projects. Feel free to refer to it while we update the information in the new documentation. +There is a [Meteor Cordova guide](/about/cordova) available that offers advanced configuration details for Meteor Cordova projects. Feel free to refer to it while we update the information in the new documentation. This section will summarize the steps needed to set up your environment for Meteor Cordova development, manage development, and generate native artifacts for store uploads. @@ -218,7 +218,7 @@ For development, enable HCP by starting the application server with the `--mobil - On a real device, both the device and server must be on the same network - Run: `meteor run android --mobile-server XXX.XXX.XXX.XXX`, replacing the IP with your local development address (e.g. 192.168.1.4). -For production, HCP is enabled automatically when you provide the `--server` option to the [`meteor build` command](../cli/index.md#meteor-build-meteorbuild). For more details on how HCP works with apps already published to production, see [Hot Code Push on mobile](https://guide.meteor.com/cordova.html#hot-code-push). +For production, HCP is enabled automatically when you provide the `--server` option to the [`meteor build` command](../cli/index.md#meteor-build-meteorbuild). For more details on how HCP works with apps already published to production, see [Hot Code Push on mobile](/troubleshooting/hot-code-push). ### Open IDE diff --git a/v3-docs/docs/about/install.md b/v3-docs/docs/about/install.md index e19d252106..c0170b5d2c 100644 --- a/v3-docs/docs/about/install.md +++ b/v3-docs/docs/about/install.md @@ -31,7 +31,7 @@ And it will prompt you to choose a project name and frontend framework. ## Installation -Install the latest official version of Meteor.js from your terminal by running one of the commands below. You can check our [changelog](https://v3-docs.meteor.com/history.html) for the release notes. +Install the latest official version of Meteor.js from your terminal by running one of the commands below. You can check our [changelog](/history) for the release notes. For Windows, Linux and OS X, you can run the following command: diff --git a/v3-docs/docs/about/modern-build-stack.md b/v3-docs/docs/about/modern-build-stack.md index f0d23ecad0..519c64805d 100644 --- a/v3-docs/docs/about/modern-build-stack.md +++ b/v3-docs/docs/about/modern-build-stack.md @@ -55,6 +55,8 @@ On first run, the package installs the required Rspack setup at the project leve ## Learn more +📄 [Build System guide](/about/build-tool) — In-depth guide covering Meteor's build tool, JavaScript transpilation, CSS processing, HMR, and build plugins. + 📹 [Modern Build Stack in Meteor 3: Empower Your Meteor Apps with Faster, Feature-Rich Bundling](https://www.youtube.com/watch?v=LqU1eDbnG4I) Presented by [@nachocodoner](https://github.com/nachocodoner) at [Meteor Impact](https://impact.meteorjs.community/), this video covers the motivation behind this new era of Meteor bundler optimizations and modernization with Rspack integration, what initially drove us there, what we have achieved, and how you can adopt it today. It was recorded while Meteor 3.4 was in beta, but the content is still accurate. The only difference is that you can now upgrade directly to the official Meteor 3.4 release. diff --git a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md index adcfb1fdeb..ede3e15917 100644 --- a/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md +++ b/v3-docs/docs/about/modern-build-stack/rspack-bundler-integration.md @@ -221,7 +221,7 @@ Meteor entry points allow a modular, modern, bundler-compliant structure for you } ``` -Learn more in [“Modular application structure” in Meteor](https://docs.meteor.com/packages/modules.html#modular-application-structure). +Learn more in [“Modular application structure” in Meteor](/packages/modules#modular-application-structure). Ensure your app defines these entry files with the correct paths where each module is expected to load. Organize your app so the loading order of modules is clear. diff --git a/v3-docs/docs/about/roadmap.md b/v3-docs/docs/about/roadmap.md index 6c14232f36..e401b90ed9 100644 --- a/v3-docs/docs/about/roadmap.md +++ b/v3-docs/docs/about/roadmap.md @@ -118,4 +118,4 @@ Beyond these, we also track smaller tasks delivered in each release. These focus --- -For more completed items, refer to our [changelog](https://docs.meteor.com/history.html). +For more completed items, refer to our [changelog](/history). diff --git a/v3-docs/docs/about/web-apps.md b/v3-docs/docs/about/web-apps.md index c3f978d10a..629746e616 100644 --- a/v3-docs/docs/about/web-apps.md +++ b/v3-docs/docs/about/web-apps.md @@ -52,4 +52,4 @@ If you want detailed help about a specific command, run `meteor help `. - Follow the [React](/tutorials/react/index.html) or [Vue](/tutorials/vue/meteorjs3-vue3.html) tutorials. New tutorials are coming soon. - Learn about [Modern Build Stack](/about/modern-build-stack.md) for faster development, smaller bundle sizes, and more. - Read about [Cordova for Mobile Apps](/about/cordova.html). -- Explore the [Meteor Guide](https://guide.meteor.com/). +- Explore the [Tutorials](/tutorials/react/index). diff --git a/v3-docs/docs/api/accounts.md b/v3-docs/docs/api/accounts.md index 233eca7454..a9e1d08cfe 100644 --- a/v3-docs/docs/api/accounts.md +++ b/v3-docs/docs/api/accounts.md @@ -15,7 +15,7 @@ login provider packages: `accounts-password`, `accounts-facebook`, `accounts-github`, `accounts-google`, `accounts-meetup`, `accounts-twitter`, or `accounts-weibo`. -Read more about customizing user accounts in the [Accounts](http://guide.meteor.com/accounts.html) article in the Meteor Guide. +Read more about customizing user accounts in the [Accounts](/tutorials/accounts/accounts) article in the Meteor Guide. ### Accounts with Session Storage {#accounts-session-storage} @@ -991,12 +991,173 @@ be called. To customize the contents of the email, see [`Accounts.emailTemplates`](#Accounts-emailTemplates). +## Email Link Callbacks and URL Customization + +When Meteor sends account-related emails, those emails contain URLs that users click +to complete actions like password reset. This section explains how these URLs work +and how to customize them. + +### How Email URLs Work + +By default, Meteor generates URLs using hash fragments: + +- `https://yourapp.com/#/reset-password/TOKEN` +- `https://yourapp.com/#/verify-email/TOKEN` +- `https://yourapp.com/#/enroll-account/TOKEN` + +**Security Note:** Hash fragments (the part after `#`) are intentionally used because +they are never sent to the server in HTTP requests. This prevents sensitive tokens +from appearing in server logs, proxy logs, or HTTP referrer headers. + +When a user clicks these links, Meteor's client-side code automatically parses +`window.location.hash` and triggers the appropriate callback registered with +the functions below. + +### Complete Example: Custom Password Reset Flow + +Here's how to implement password reset without `accounts-ui`: + +```js +// client/accounts-hooks.js +import { Accounts } from 'meteor/accounts-base'; + +// Register at top level, NOT inside Meteor.startup() +let doneCallback; + +Accounts.onResetPasswordLink((token, done) => { + // Store token and done callback for your UI + Session.set('resetPasswordToken', token); + doneCallback = done; + + // Show your password reset form + // The login process is suspended until done() is called +}); + +// In your password reset form submit handler: +function submitNewPassword(newPassword) { + const token = Session.get('resetPasswordToken'); + + Accounts.resetPassword(token, newPassword, (error) => { + if (error) { + alert('Reset failed: ' + error.reason); + } else { + Session.set('resetPasswordToken', null); + doneCallback(); // Re-enables auto-login + } + }); +} +``` + +### Customizing Email URLs + + + +`Accounts.urls` is a server-side object containing functions that generate URLs +for account emails. Override these to customize the URL format. + +| Property | Signature | Description | +|----------|-----------|-------------| +| `resetPassword` | `(token, extraParams?) => string` | Password reset URL | +| `verifyEmail` | `(token, extraParams?) => string` | Email verification URL | +| `enrollAccount` | `(token, extraParams?) => string` | Account enrollment URL | +| `loginToken` | `(selector, token, extraParams?) => string` | Login token URL | + +#### Async URL Generation + +The URL methods can also return **Promises** that resolve to strings. This is useful when +URL generation requires asynchronous operations, such as: +- Looking up user data from the database +- Calling external services (e.g., URL shorteners) +- Generating signed URLs from cloud providers + +The email-sending functions (`Accounts.sendResetPasswordEmail`, `Accounts.sendEnrollmentEmail`, +and `Accounts.sendVerificationEmail`) handle both synchronous and asynchronous URL methods +transparently. + +**Example: Async URL with database lookup** + +```js +// Server-side +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; + +Accounts.urls.resetPassword = async (token, extraParams) => { + // Example: Look up user preference for custom domain + const user = await Meteor.users.findOneAsync({ 'services.password.reset.token': token }); + const domain = user?.profile?.preferredDomain || Meteor.absoluteUrl(); + + return `${domain}reset-password/${token}`; +}; +``` + +**Example: Using a URL shortener service** + +```js +// Server-side +Accounts.urls.verifyEmail = async (token) => { + const longUrl = Meteor.absoluteUrl(`verify-email/${token}`); + + // Shorten the URL using an external service + const shortUrl = await shortenUrl(longUrl); + return shortUrl; +}; +``` + +**Example: Using Clean URLs Instead of Hash Fragments** + +If your router doesn't handle hash fragments well, you can override `Accounts.urls` +to use clean URLs: + +```js +// Server-side +import { Accounts } from 'meteor/accounts-base'; +import { Meteor } from 'meteor/meteor'; + +Accounts.urls.resetPassword = (token) => { + return Meteor.absoluteUrl(`reset-password/${token}`); +}; + +Accounts.urls.verifyEmail = (token) => { + return Meteor.absoluteUrl(`verify-email/${token}`); +}; + +Accounts.urls.enrollAccount = (token) => { + return Meteor.absoluteUrl(`enroll-account/${token}`); +}; +``` + +**Important:** When using clean URLs (without `#/`), the built-in +`Accounts.onResetPasswordLink`, `Accounts.onEnrollmentLink`, and +`Accounts.onEmailVerificationLink` callbacks won't work automatically. +Handle tokens in your router instead: + +```js +// Example with a router +Router.route('/reset-password/:token', function() { + const token = this.params.token; + // Show password reset UI, call Accounts.resetPassword(token, newPassword) +}); +``` + +### Router Integration + +You have three options when integrating with client-side routers: + +1. **Keep default hash URLs** - Works out of the box + with `Accounts.on*Link` callbacks. No router configuration needed. + +2. **Override `Accounts.urls` for clean URLs** - More "modern" looking URLs, + but requires handling tokens in your router. + +3. **Use hashbang mode** - Some routers support `#!/` routes. Configure your + router accordingly and update `Accounts.urls` to use `#!/` instead of `#/`. + This is an `Object` with several fields that are used to generate text/html diff --git a/v3-docs/docs/api/collections.md b/v3-docs/docs/api/collections.md index b403eb9fdd..70ce0492fd 100644 --- a/v3-docs/docs/api/collections.md +++ b/v3-docs/docs/api/collections.md @@ -198,7 +198,7 @@ Greetings.findOne({ name: 'John' }); // 🧾 Data is available (Optimistic-UI) Read more about server and stub promises on calling methods, [please refer to the docs](./meteor.md#Meteor-callAsync). -Read more about collections and how to use them in the [Collections](http://guide.meteor.com/collections.html) article in the Meteor Guide. +Read more about collections and how to use them in the [Collections](/tutorials/collections/collections) article in the Meteor Guide. @@ -508,7 +508,7 @@ While `allow` and `deny` make it easy to get started building an app, it's harder than it seems to write secure `allow` and `deny` rules. We recommend that developers avoid `allow` and `deny`, and switch directly to custom methods once they are ready to remove `insecure` mode from their app. See -[the Meteor Guide on security](https://guide.meteor.com/security.html#allow-deny) +[the Meteor Guide on security](/tutorials/security/security#allow-deny) for more details. ::: @@ -649,7 +649,7 @@ While `allow` and `deny` make it easy to get started building an app, it's harder than it seems to write secure `allow` and `deny` rules. We recommend that developers avoid `allow` and `deny`, and switch directly to custom methods once they are ready to remove `insecure` mode from their app. See -[the Meteor Guide on security](https://guide.meteor.com/security.html#allow-deny) +[the Meteor Guide on security](/tutorials/security/security#allow-deny) for more details. ::: @@ -671,7 +671,11 @@ The methods (like `update` or `insert`) you call on the resulting _raw_ collecti ## Collection Extensions -Meteor provides a powerful Collection Extensions API that allows you to extend the functionality of all collection instances. These static methods on `Mongo.Collection` let you add constructor extensions, prototype methods, and static methods to customize collection behavior. These very same APIs are exported under `CollectionExtensions` for backwards compatibility with [lai:collection-extensions](https://github.com/Meteor-Community-Packages/meteor-collection-extensions). +**Integrated into core in Meteor 3.4** ([PR#13830](https://github.com/meteor/meteor/pull/13830)) + +Meteor provides a powerful Collection Extensions API that allows you to extend the functionality of all collection instances. These static methods on `Mongo.Collection` let you add constructor extensions, prototype methods, and static methods to customize collection behavior. + +These APIs were previously available through the community package [lai:collection-extensions](https://github.com/Meteor-Community-Packages/meteor-collection-extensions) and are now integrated directly into Meteor core. The same APIs are exported under `CollectionExtensions` for backwards compatibility. @@ -744,13 +748,13 @@ Get all registered static methods. Returns a Map of method names to functions. U ### Legacy Aliases - +### Mongo.Collection.addPrototype -Backwards compatibility alias for `addPrototypeMethod`. **Deprecated** - use `addPrototypeMethod` instead. +> **Deprecated** — backwards compatibility alias for [`addPrototypeMethod`](#Mongo-Collection-addPrototypeMethod). Use `addPrototypeMethod` instead. - +### Mongo.Collection.removePrototype -Backwards compatibility alias for `removePrototypeMethod`. **Deprecated** - use `removePrototypeMethod` instead. +> **Deprecated** — backwards compatibility alias for [`removePrototypeMethod`](#Mongo-Collection-removePrototypeMethod). Use `removePrototypeMethod` instead. ## Cursors {#mongo_cursor} diff --git a/v3-docs/docs/api/meteor.md b/v3-docs/docs/api/meteor.md index 911bf378fd..68f37fd31a 100644 --- a/v3-docs/docs/api/meteor.md +++ b/v3-docs/docs/api/meteor.md @@ -88,7 +88,10 @@ Using this pattern can get some performance gains on the defined environments as this can increase the speed of startup. -This helper function allows you to defer the execution of a function only in development environments. + +**Introduced in Meteor 3.4** ([PR#14006](https://github.com/meteor/meteor/pull/14006)) + +This helper function allows you to defer the execution of a function only in development environments, significantly improving server startup times in development by deferring non-critical setup code. ::: code-group @@ -283,7 +286,7 @@ to each method call on the client, and checking on the server whether a call with this ID has already been made. Alternatively, you can use [`Meteor.apply`](#Meteor-apply) with the noRetry option set to true. -Read more about methods and how to use them in the [Methods](http://guide.meteor.com/methods.html) article in the Meteor Guide. +Read more about methods and how to use them in the [Methods](/tutorials/methods/methods) article in the Meteor Guide. @@ -730,7 +733,7 @@ will still work. ::: Read more about publications and how to use them in the -[Data Loading](http://guide.meteor.com/data-loading.html) article in the Meteor Guide. +[Data Loading](/tutorials/data-loading/data-loading) article in the Meteor Guide. diff --git a/v3-docs/docs/cli/index.md b/v3-docs/docs/cli/index.md index 0e73eb614c..efb8527bea 100644 --- a/v3-docs/docs/cli/index.md +++ b/v3-docs/docs/cli/index.md @@ -276,7 +276,7 @@ If you run `meteor create` without arguments, Meteor will launch an interactive | `--blaze` | Basic Blaze app | [Meteor 2 with Blaze](https://blaze-tutorial.meteor.com/) | | `--solid` | Solid | [Meteor 2 with Solid Example](https://github.com/fredmaiaarantes/meteor-solid-app/releases/tag/milestone-2.0) | | `--apollo` | React + Apollo (GraphQL) | [Meteor 2 with GraphQL](https://react-tutorial.meteor.com/simple-todos-graphql/) | -| `--typescript` | React + TypeScript | [TypeScript Guide](https://guide.meteor.com/build-tool.html#typescript) | +| `--typescript` | React + TypeScript | [TypeScript Guide](/about/build-tool#typescript) | | `--tailwind` | React + Tailwind CSS | - | | `--chakra-ui` | React + Chakra UI | [Simple Tasks Example](https://github.com/fredmaiaarantes/simpletasks) | | `--coffeescript` | CoffeeScript | - | @@ -293,7 +293,7 @@ If you run `meteor create` without arguments, Meteor will launch an interactive | `--package` | Create a new package instead of an application | ::: warning Prototype Mode -The `--prototype` option adds packages that make development faster but shouldn't be used in production. See the [security checklist](https://guide.meteor.com/security.html#checklist). +The `--prototype` option adds packages that make development faster but shouldn't be used in production. See the [security checklist](/tutorials/security/security#checklist). ::: ### Included Packages @@ -358,7 +358,7 @@ The `--prototype` option adds packages that make development faster but shouldn' ::: tip File Structure -To learn more about the recommended file structure for Meteor apps, check the [Meteor Guide](https://guide.meteor.com/structure.html#javascript-structure). +To learn more about the recommended file structure for Meteor apps, check the [Meteor Guide](/tutorials/application-structure/#javascript-structure). ::: ## meteor generate {meteorgenerate} diff --git a/v3-docs/docs/community-packages/jam-method.md b/v3-docs/docs/community-packages/jam-method.md index 0958974d6c..a8aff2ec54 100644 --- a/v3-docs/docs/community-packages/jam-method.md +++ b/v3-docs/docs/community-packages/jam-method.md @@ -262,7 +262,7 @@ You can also set all methods to be `serverOnly`. See [Configuring](#configuring- 1. Attach methods to its Collection with a dynamic import as shown above [Attach methods to its Collection (optional)](#attach-methods-to-its-collection-optional) -2. Import function(s) from a file within a `/server` folder. Any code imported from a `/server` folder will not be shipped to the client. The `/server` folder can be located anywhere within your project's file structure and you can have multiple `/server` folders. For example, you can co-locate with your collection folder, e.g. `/imports/api/todos/server/`, or it can be at the root of your project. See [Secret server code](https://guide.meteor.com/security.html#secret-code) for more info. +2. Import function(s) from a file within a `/server` folder. Any code imported from a `/server` folder will not be shipped to the client. The `/server` folder can be located anywhere within your project's file structure and you can have multiple `/server` folders. For example, you can co-locate with your collection folder, e.g. `/imports/api/todos/server/`, or it can be at the root of your project. See [Secret server code](/tutorials/security/security#secret-code) for more info. ```js export const create = createMethod({ diff --git a/v3-docs/docs/community-packages/pub-sub.md b/v3-docs/docs/community-packages/pub-sub.md index 7015efe054..0eca082b2f 100644 --- a/v3-docs/docs/community-packages/pub-sub.md +++ b/v3-docs/docs/community-packages/pub-sub.md @@ -218,7 +218,7 @@ Meteor.subscribe('notes.all', { cache: true }) // turns caching on, overriding t > **Note**: the rest of the [Meteor.subscribe](https://docs.meteor.com/api/pubsub.html#Meteor-subscribe) API (e.g. `onStop`, `onReady`) works just as you'd expect. -> **Note**: Because the data will remain in Minimongo while the subscription is cached, you should be mindful of your Minimongo `.find` selectors. Be sure to use specific selectors to `.find` the data you need for that particular subscription. This is generally considered [best practice](https://guide.meteor.com/data-loading#fetching) so this is mainly a helpful reminder. +> **Note**: Because the data will remain in Minimongo while the subscription is cached, you should be mindful of your Minimongo `.find` selectors. Be sure to use specific selectors to `.find` the data you need for that particular subscription. This is generally considered [best practice](/tutorials/data-loading/data-loading#fetching) so this is mainly a helpful reminder. #### Clearing the cache Each individual subcription will be automatically removed from the cache when its `cacheDuration` elapses. diff --git a/v3-docs/docs/generators/meteor-versions/metadata.generated.js b/v3-docs/docs/generators/meteor-versions/metadata.generated.js index 7cc845b19a..a7ae0d966d 100644 --- a/v3-docs/docs/generators/meteor-versions/metadata.generated.js +++ b/v3-docs/docs/generators/meteor-versions/metadata.generated.js @@ -46,9 +46,13 @@ export default { }, { "version": "v3.3.2", - "url": "https://release-3-3-2.docs.meteor.com/", + "url": "https://release-3-3-2.docs.meteor.com/" + }, + { + "version": "v3.4.0", + "url": "https://release-3-4-0.docs.meteor.com/", "isCurrent": true } ], - "currentVersion": "v3.3.2" + "currentVersion": "v3.4.0" } \ No newline at end of file diff --git a/v3-docs/docs/packages/1.when-to-use-meteor-packages.md b/v3-docs/docs/packages/1.when-to-use-meteor-packages.md index 0b6c6d50a1..2df47e8e3c 100644 --- a/v3-docs/docs/packages/1.when-to-use-meteor-packages.md +++ b/v3-docs/docs/packages/1.when-to-use-meteor-packages.md @@ -11,4 +11,4 @@ Atmosphere packages are packages written specifically for Meteor and have severa - Include [build plugins](https://docs.meteor.com/api/package.html#build-plugin-api) for Meteor's build system - Include pre-built binary code for different server architectures, such as Linux or Windows -If your package depends on another Atmosphere package, or needs to take advantage of Meteor's [build system](https://docs.meteor.com/about/modern-build-stack.html#modern-build-stack), writing an Atmosphere package might be the best option for now. +If your package depends on another Atmosphere package, or needs to take advantage of Meteor's [build system](/about/build-tool), writing an Atmosphere package might be the best option for now. diff --git a/v3-docs/docs/packages/3.writing-atmosphere-packages.md b/v3-docs/docs/packages/3.writing-atmosphere-packages.md deleted file mode 100644 index 7018cb6fca..0000000000 --- a/v3-docs/docs/packages/3.writing-atmosphere-packages.md +++ /dev/null @@ -1,341 +0,0 @@ -## Writing Atmosphere packages - -To get started writing a package, use the Meteor command line tool: - -```bash -meteor create --package my-package -``` -> It is required that your `my-package` name take the form of `username:my-package`, where `username` is your Meteor Developer username, if you plan to publish your package to Atmosphere. - -If you run this inside an app, it will place the newly generated package in that app's `packages/` directory. Outside an app, it will just create a standalone package directory. The command also generates some boilerplate files for you: - -```txt -my-package -├── README.md -├── package.js -├── my-package-tests.js -└── my-package.js -``` - -The `package.js` file is the main file in every Meteor package. This is a JavaScript file that defines the metadata, files loaded, architectures, npm packages, and Cordova packages for your Meteor package. - -In this tutorial, we will go over some important points for building packages, but we won't explain every part of the `package.js` API. To learn about all of the options, [read about the `package.js` API in the Meteor docs.](https://docs.meteor.com/api/package.html) - -> Don't forget to run [`meteor add [my-package]`](https://docs.meteor.com/cli/#meteor-add) once you have finished developing your package in order to use it; this applies if the package is a local package for internal use only or if you have published the package to Atmosphere. - -### Adding files and assets - -The main function of an Atmosphere package is to contain source code (JS, CSS, and any transpiled languages) and assets (images, fonts, and more) that will be shared across different applications. - -### Adding JavaScript - -To add JavaScript files to a package, specify an entrypoint with [`api.mainModule()`](https://docs.meteor.com/packages/modules.html#modular-package-structure) in the package's `onUse` block (this will already have been done by `meteor create --package` above): - -```js -Package.onUse(function(api) { - api.mainModule('my-package.js'); -}); -``` - -From that entrypoint, you can `import` other files within your package, [just as you would in an application](https://docs.meteor.com/packages/modules.html). - -If you want to include different files on the client and server, you can specify multiple entry points using the second argument to the function: - -```js -Package.onUse(function(api) { - api.mainModule('my-package-client.js', 'client'); - api.mainModule('my-package-server.js', 'server'); -}); -``` - -You can also add any source file that would be compiled to a JS file (such as a CoffeeScript file) in a similar way, assuming you [depend](#dependencies) on an appropriate build plugin. - -

Adding CSS

- -To include CSS files with your package you can use [`api.addFiles()`](https://docs.meteor.com/api/package.html#PackageAPI-addFiles): - -```js -Package.onUse(function(api) { - api.addFiles('my-package.css', 'client'); -}); -``` - -The CSS file will be automatically loaded into any app that uses your package. - -### Adding Sass, Less, or Stylus mixins/variables - -Just like packages can export JavaScript code, they can export reusable bits of CSS pre-processor code. You can also have a package that doesn't actually include any CSS, but just exports different bits of reusable mixins and variables. To get more details see Meteor [build tool CSS pre-processors](https://guide.meteor.com/build-tool.html#css): - -```js -Package.onUse(function(api) { - api.addFiles('my-package.scss', 'client'); -}); -``` - -This Sass file will be eagerly evaluated and its compiled form will be added to the CSS of the app immediately. - -```js -Package.onUse(function(api) { - api.addFiles([ - 'stylesheets/_util.scss', - 'stylesheets/_variables.scss' - ], 'client', {isImport: true}); -}); -``` - -These two Sass files will be lazily evaluated and only included in the CSS of the app if imported from some other file. - -### Adding other assets - -You can include other assets, such as fonts, icons or images, to your package using [`api.addAssets`](https://docs.meteor.com/api/package.html#PackageAPI-addAssets): - -```js -Package.onUse(function(api) { - api.addAssets([ - 'font/OpenSans-Regular-webfont.eot', - 'font/OpenSans-Regular-webfont.svg', - 'font/OpenSans-Regular-webfont.ttf', - 'font/OpenSans-Regular-webfont.woff', - ], 'client'); -}); -``` - -You can then access these files from the client from a URL `/packages/username_my-package/font/OpenSans-Regular-webfont.eot` or from the server using the [Assets API](https://docs.meteor.com/api/assets.html#Assets-getTextAsync). - -### Exporting - -While some packages exist just to provide side effects to the app, most packages provide a reusable bit of code that can be used by the consumer with `import`. To export a symbol from your package, use the ES2015 `export` syntax in your `mainModule`: - -```js -// in my-package.js: -export const myName = 'my-package'; -``` - -Now users of your package can import the symbol with: - -```js -import { myName } from 'meteor/username:my-package'; -``` - -### Dependencies - -Chances are your package will want to make use of other packages. To ensure they are available, you can declare dependencies. Atmosphere packages can depend both on other Atmosphere packages, as well as packages from npm. - -#### Atmosphere dependencies - -To depend on another Atmosphere package, use [`api.use`](https://docs.meteor.com/api/package.html#PackageAPI-use): - -```js -Package.onUse(function(api) { - // This package depends on 1.2.0 or above of validated-method - api.use('mdg:validated-method@1.2.0'); -}); -``` - -One important feature of the Atmosphere package system is that it is single-loading: no two packages in the same app can have dependencies on conflicting versions of a single package. Read more about that in the section about version constraints below. - -#### Depending on Meteor version - -Note that the Meteor release version number is mostly a marketing artifact - the core Meteor packages themselves typically don't share this version number. This means packages can only depend on specific versions of the packages inside a Meteor release, but can't depend on a specific release itself. We have a helpful shorthand api called [`api.versionsFrom`](https://docs.meteor.com/api/package.html#PackageAPI-versionsFrom) that handles this for you by automatically filling in package version numbers from a particular release: - -```js -// Use versions of core packages from Meteor 1.2.1 -api.versionsFrom('1.2.1'); - -api.use([ - // Don't need to specify version because of versionsFrom above - 'ecmascript', - 'check', - - // Still need to specify versions of non-core packages - 'mdg:validated-method@1.2.0', - 'mdg:validation-error@0.1.0' -]); -``` - -The above code snippet is equivalent to the code below, which specifies all of the version numbers individually: - -```js -api.use([ - 'ecmascript@0.1.6', - 'check@1.1.0', - 'mdg:validated-method@1.2.0', - 'mdg:validation-error@0.1.0' -]); -``` - -Additionally, you can call `api.versionsFrom()` multiple times, or with -an array (eg `api.versionsFrom([, ])`. Meteor will interpret -this to mean that the package will work with packages from all the listed releases. - -```js -api.versionsFrom('1.2.1'); -api.versionsFrom('1.4'); -api.versionsFrom('1.8'); - -// or - -api.versionsFrom(['1.2.1', '1.4', '1.8']); -``` - -This usually isn't necessary, but can help in cases where you support more than -one major version of a core package. - - -#### Semantic versioning and version constraints - -Meteor's package system relies heavily on [Semantic Versioning](http://semver.org/), or SemVer. When one package declares a dependency on another, it always comes with a version constraint. These version constraints are then solved by Meteor's industrial-grade Version Solver to arrive at a set of package versions that meet all of the requirements, or display a helpful error if there is no solution. - -The mental model here is: - -1. **The major version must always match exactly.** If package `a` depends on `b@2.0.0`, the constraint will only be satisfied if the version of package `b` starts with a `2`. This means that you can never have two different major versions of a package in the same app. -2. **The minor and patch version numbers must be greater or equal to the requested version.** If the dependency requests version `2.1.3`, then `2.1.4` and `2.2.0` will work, but `2.0.4` and `2.1.2` will not. - -The constraint solver is necessary because Meteor's package system is **single-loading** - that is, you can never have two different versions of the same package loaded side-by-side in the same app. This is particularly useful for packages that include a lot of client-side code, or packages that expect to be singletons. - -Note that the version solver also has a concept of "gravity" - when many solutions are possible for a certain set of dependencies, it always selects the oldest possible version. This is helpful if you are trying to develop a package to ship to lots of users, since it ensures your package will be compatible with the lowest common denominator of a dependency. If your package needs a newer version than is currently being selected for a certain dependency, you need to update your `package.js` to have a newer version constraint. - -If your package supports multiple major versions of a dependency, you can supply -both versions to `api.use` like so: - -```js -api.use('blaze@1.0.0 || 2.0.0'); -``` - -Meteor will use whichever major version is compatible with your other packages, -or the most recent of the options given. - -#### npm dependencies - -Meteor packages can include [npm packages](https://www.npmjs.com/) to use JavaScript code from outside the Meteor package ecosystem or to include JavaScript code with native dependencies. Use [Npm.depends](https://docs.meteor.com/api/package.html#Npm-require) at the top level of your `package.js` file. For example, here's how you would include the `github` npm package: - -```js -Npm.depends({ - github: '0.2.4' -}); -``` - -If you want to use a local npm package, for example during development, you can give a directory instead of a version number: - -```js -Npm.depends({ - my-package: 'file:///home/user/npms/my-package' -}); -``` - -If you want to include a npm package only during development: - -```js -Npm.devDepends({ - github: '0.2.4' -}); -``` - -You can import the dependency from within you package code in the same way that you would inside an application: - -```js -import github from 'github'; -``` - -#### Peer npm dependencies - -`Npm.depends()` is fairly rigid (you can only depend on an exact version), and will typically result in multiple versions of a package being installed if many different Atmosphere packages depend on the same npm package. This makes it less than ideal to use on the client, where it's impractical to ship multiple copies of the same package code to the browser. Client-side packages are also often written with the assumption that only a single copy will be loaded. For example, React will complain if it is included more than once in an application bundle. - -To avoid this problem as a package author, you can request that users of your package have installed the npm package you want to use at the application level. This is similar to a [peer dependency](https://nodejs.org/en/blog/npm/peer-dependencies/) of an npm package (although with less support in the tool). You can use the [`tmeasday:check-npm-versions`](https://atmospherejs.com/tmeasday/check-npm-versions) package to ensure that they've done this, and to warn them if not. - -For instance, if you are writing a React package, you should not directly depend on [`react`](https://www.npmjs.com/package/react), but instead use `check-npm-versions` to check the user has installed it: - -```js -import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; - -checkNpmVersions({ - 'react': '0.14.x' -}, 'my:awesome-package'); - -// If you are using the dependency in the same file, you'll need to use require, otherwise -// you can continue to `import` in another file. -const React = require('react'); -``` - -> Note that `checkNpmVersions` will only output a warning if the user has installed a incompatible version of the npm package. So your `require` call may not give you what you expect. This is consistent with npm's handling of [peer dependencies](http://blog.npmjs.org/post/110924823920/npm-weekly-5). - -### Cordova plugins - -Atmosphere packages can include [Cordova plugins](http://cordova.apache.org/plugins/) to ship native code for the Meteor mobile app container. This way, you can interact with the native camera interface, use the gyroscope, save files locally, and more. - -Include Cordova plugins in your Meteor package by using [Cordova.depends](https://docs.meteor.com/api/package.html#PackageCordova-depends). - -Read more about using Cordova in the [mobile guide](https://guide.meteor.com/mobile.html). - -### Testing packages - -Meteor has a test mode for packages called `meteor test-packages`. If you are in a package's directory, you can run - -```bash -meteor test-packages ./ --driver-package meteortesting:mocha -``` - -This will run a special app containing only a "test" version of your package and start a Mocha [test driver package](https://guide.meteor.com/testing#driver-packages). - -When your package starts in test mode, rather than loading the `onUse` block, Meteor loads the `onTest` block: - -```js -Package.onTest(function(api) { - // You almost definitely want to depend on the package itself, - // this is what you are testing! - api.use('my-package'); - - // You should also include any packages you need to use in the test code - api.use(['ecmascript', 'random', 'meteortesting:mocha']); - - // Finally add an entry point for tests - api.mainModule('my-package-tests.js'); -}); -``` - -From within your test entry point, you can import other files as you would in the package proper. - -You can read more about testing in Meteor in the [Testing article](https://guide.meteor.com/testing). - -### Publishing your package - -To publish your package to Atmosphere, run [`meteor publish`](https://docs.meteor.com/cli/#meteorpublish) from the package directory. To publish a package the package name must follow the format of `username:my-package` and the package must contain a [SemVer version number](#semantic-versionning-and-version-constraints). - -> Note that if you have a local `node_modules` directory in your package, remove it before running `meteor publish`. While local `node_modules` directories are allowed in Meteor packages, their paths can collide with the paths of `Npm.depends` dependencies when published. - -## Dev only packages - -If you want to implement a package used only during development and ensure it’s excluded from the production build (`meteor build`), you can do so by adding `devOnly: true` in the package description at `package.js`. - -```js -Package.describe({ - summary: 'My dev-only package', - version: '1.0.0', - name: 'username:dev-package', - // Mark the package as devOnly - devOnly: true, -}); -``` - -## Cache format - -If you've ever looked inside Meteor's package cache at `~/.meteor/packages`, you know that the on-disk format of a built Meteor package is completely different from the way the source code looks when you're developing the package. The idea is that the target format of a package can remain consistent even if the API for development changes. - -## Local packages - -As an alternative to publishing your package on Atmosphere, if you want to keep your package private, you can place it in your Meteor app in the `packages/` directory, for instance `packages/foo/`, and then add it to your app with `meteor add foo`. - -Using `git submodules` to handle private packages is a common pattern. - -### Overriding published packages with a local version - -If you need to modify an Atmosphere package to do something that the published version doesn't do, you can edit a local version of the package on your computer. - -A Meteor app can load Atmosphere packages in one of three ways, and it looks for a matching package name in the following order: - -1. Package source code in the `packages/` directory inside your app. -2. Package source code in directories indicated by setting a `METEOR_PACKAGE_DIRS` environment variable before running any `meteor` command. You can add multiple directories by separating the paths with a `:` on OSX or Linux, or a `;` on Windows. For example: `METEOR_PACKAGE_DIRS=../first/directory:../second/directory`, or on Windows: `set PACKAGE_DIRS=..\first\directory;..\second\directory`. -> Note: Prior to Meteor 1.4.2, `METEOR_PACKAGE_DIRS` was `PACKAGE_DIRS`. For compatibility reasons, developers should use `METEOR_PACKAGE_DIRS` going forward. -3. Pre-built package from Atmosphere. The package is cached in `~/.meteor/packages` on Mac/Linux or `%LOCALAPPDATA%\.meteor\packages` on Windows, and only loaded into your app as it is built. - -You can use (1) or (2) to override the version from Atmosphere. You can even do this to load patched versions of Meteor core packages - just copy the code of the package from [Meteor's GitHub repository](https://github.com/meteor/meteor/tree/devel/packages), and edit away. diff --git a/v3-docs/docs/packages/4.using-npm-packages.md b/v3-docs/docs/packages/4.using-npm-packages.md new file mode 100644 index 0000000000..b67f03798e --- /dev/null +++ b/v3-docs/docs/packages/4.using-npm-packages.md @@ -0,0 +1,258 @@ +# Using npm Packages + +This guide covers how to find, install, and use npm packages in your Meteor application. + +## Searching for packages + +You can find npm packages using: + +- [npmjs.com](https://www.npmjs.com/) - The official npm registry +- [npms.io](https://npms.io/) - Searches by package quality, maintenance status, and popularity + +## npm on the client + +Meteor uses bundling technology similar to webpack to provide a Node-like environment on the client, allowing many npm packages intended for the server to run in the browser. + +When creating a new application, Meteor installs the `meteor-node-stubs` npm package to provide browser-friendly implementations of Node's built-in modules like `path`, `buffer`, `util`, etc. + +::: info +Meteor's module system avoids bundling any stub modules if they are not used, so there is no cost to keeping `meteor-node-stubs` in your dependencies. +::: + +## Installing npm packages + +npm packages are configured in a `package.json` file at the root of your project. If you create a new Meteor project, you will have such a file created for you. If not, you can run `meteor npm init` to create one. + +To install a package into your app: + +```bash +meteor npm install --save moment +``` + +This will: +1. Update your `package.json` with information about the dependency +2. Download the package into your app's local `node_modules` directory + +Typically, you don't check the `node_modules` directory into source control. Your teammates run `meteor npm install` to get up to date when dependencies change: + +```bash +meteor npm install +``` + +### Development dependencies + +If the package is just a development dependency (used for testing, linting, etc.), use `--save-dev`: + +```bash +meteor npm install --save-dev eslint +``` + +This way, production builds can run `npm install --production` and avoid installing packages they don't need. + +::: tip +Meteor comes with npm bundled so that you can type `meteor npm` without worrying about installing it yourself. You can also use a globally installed npm if you prefer. +::: + +## Using npm packages + +To use an npm package from a file in your application, use `import`: + +```js +import moment from 'moment'; + +// This is equivalent to the standard Node.js require: +const moment = require('moment'); +``` + +This imports the default export from the package into the symbol `moment`. + +You can also import specific functions using destructuring: + +```js +import { isArray } from 'lodash'; +``` + +You can also import other files or entry points from a package: + +```js +import { parse } from 'graphql/language'; +``` + +### Importing styles from npm + +Using any of Meteor's supported CSS pre-processors, you can import style files from npm packages. + +Importing with an absolute path using the `{}` syntax (with Less): + +```less +@import '{}/node_modules/npm-package-name/button.less'; +``` + +Importing with a relative path: + +```less +@import '../../node_modules/npm-package-name/colors.less'; +``` + +You can also import CSS directly from a JavaScript file: + +```js +import 'npm-package-name/stylesheets/styles.css'; +``` + +::: warning +When importing CSS from a JavaScript file, that CSS is not bundled with the rest of the CSS processed with the Meteor build tool. Instead, it is put in your app's `` tag inside `` after the main concatenated CSS file. +::: + +### Building with other assets from npm + +Meteor supports building other assets (like fonts) that are located in your `node_modules` directory by symbolic linking to those assets from either the `/public` or `/private` directories: + +```bash +cd public +ln -s ../node_modules/font-awesome/fonts ./fonts +``` + +Any assets made available via symlinks in `/public` and `/private` will be copied into the Meteor application bundles when using `meteor build`. + +## Recompiling npm packages + +Meteor does not recompile packages installed in your `node_modules` by default. However, you can configure recompilation of specific npm packages through the `meteor.nodeModules.recompile` configuration object in your `package.json` file: + +```json +{ + "name": "your-application", + "meteor": { + "nodeModules": { + "recompile": { + "very-modern-package": ["client", "server"], + "alternate-notation": true, + "somewhat-modern-package": "legacy", + "another-package": ["legacy", "server"] + } + } + } +} +``` + +The keys are npm package names, and the values specify for which bundles those packages should be recompiled. + +For example, if an npm package uses modern JavaScript syntax that's fine for modern browsers and server code, but needs to be recompiled for the legacy bundle, specify `"legacy"` or `["legacy"]` as the value. + +## Using async npm packages + +Many npm packages use asynchronous, callback or promise-based APIs. In Meteor 3, you should use `async/await` patterns: + +```js +import { getData } from 'some-async-package'; + +// In a Method +Meteor.methods({ + async 'myMethod'() { + const result = await getData(); + return result; + } +}); + +// In a publication (for setup, not the return) +Meteor.publish('myPublication', async function() { + const config = await getConfigAsync(); + return MyCollection.find({ type: config.type }); +}); +``` + +### Wrapping callback-based APIs + +If you have an npm package that uses callbacks, you can wrap it with `Promise`: + +```js +import { legacyAsyncFunction } from 'some-package'; + +function promisifiedFunction(args) { + return new Promise((resolve, reject) => { + legacyAsyncFunction(args, (err, result) => { + if (err) reject(err); + else resolve(result); + }); + }); +} + +// Now you can use async/await +const result = await promisifiedFunction(myArgs); +``` + +Many Node.js core modules also provide promise-based versions via `util.promisify`: + +```js +import { promisify } from 'util'; +import { readFile } from 'fs'; + +const readFileAsync = promisify(readFile); +const contents = await readFileAsync('/path/to/file', 'utf8'); +``` + +## Package lock files + +`package.json` typically encodes a version range, so each `npm install` command can sometimes lead to different results if new versions have been published. + +To ensure your entire team uses the exact same version of each package, use `package-lock.json`: + +```bash +# The lock file is automatically created/updated when you install +meteor npm install --save moment + +# Commit package-lock.json to source control +git add package-lock.json +git commit -m "Update dependencies" +``` + +When other team members pull changes, they run: + +```bash +meteor npm install +``` + +This will use the versions specified in `package-lock.json`. + +## Peer dependencies + +Some npm packages declare peer dependencies, which means they expect certain packages to be installed by your application. If you see peer dependency warnings, you may need to install those packages: + +```bash +meteor npm install --save react react-dom +``` + +## Common issues + +### Binary dependencies + +Some npm packages include native binary components. These packages need to be rebuilt when: +- You switch Node.js versions +- You deploy to a different architecture + +If you encounter issues, try: + +```bash +meteor npm rebuild +``` + +### Module resolution + +If you're having trouble importing a package, check: +1. The package is installed in `node_modules` +2. You're using the correct import path +3. The package's `main` field in its `package.json` points to the right file + +### Version conflicts + +If you have conflicting versions of the same package, npm will try to resolve them. You can check for issues with: + +```bash +meteor npm ls +``` + +To see which packages depend on a specific package: + +```bash +meteor npm ls package-name +``` diff --git a/v3-docs/docs/packages/5.writing-npm-packages.md b/v3-docs/docs/packages/5.writing-npm-packages.md new file mode 100644 index 0000000000..16ec40cdac --- /dev/null +++ b/v3-docs/docs/packages/5.writing-npm-packages.md @@ -0,0 +1,447 @@ +# Writing npm Packages + +This guide covers how to create, develop, and publish npm packages for use in Meteor applications. + +## Creating a new npm package + +To create a new npm package: + +```bash +mkdir my-package +cd my-package/ +meteor npm init +``` + +The last command creates a `package.json` file and prompts you for the package information. You may skip everything but `name`, `version`, and `entry point`. You can use the default `index.js` for `entry point`. This file is where you set your package's exports: + +```js +// my-package/index.js +exports.myPackageLog = function() { + console.log("logged from my-package"); +}; +``` + +Now apps that include this package can do: + +```js +import { myPackageLog } from 'my-package'; + +myPackageLog(); // > "logged from my-package" +``` + +When choosing a name for your npm package, be sure to follow the [npm guidelines](https://docs.npmjs.com/files/package.json#name). + +## ES Modules and CommonJS + +Modern npm packages typically use ES Modules syntax. Here's how to set up your package for both ESM and CommonJS: + +### ES Modules package + +Create a `package.json` with `"type": "module"`: + +```json +{ + "name": "my-package", + "version": "1.0.0", + "type": "module", + "main": "index.js", + "exports": { + ".": "./index.js" + } +} +``` + +Then use ES Module syntax in your code: + +```js +// my-package/index.js +export function myPackageLog() { + console.log("logged from my-package"); +} + +export async function fetchData() { + // async operations work naturally + const result = await someAsyncOperation(); + return result; +} +``` + +### Dual ESM/CommonJS package + +To support both module systems, you can provide separate entry points: + +```json +{ + "name": "my-package", + "version": "1.0.0", + "main": "dist/cjs/index.js", + "module": "dist/esm/index.js", + "exports": { + ".": { + "import": "./dist/esm/index.js", + "require": "./dist/cjs/index.js" + } + } +} +``` + +## Including in your app + +When you are developing a new npm package for your app, there are a couple methods for including the package in your app: + +### Inside node_modules + +Place the package in your app's `node_modules/` directory, and add the package to source control. Do this when you want everything in a single repository: + +```bash +cd my-app/node_modules/ +mkdir my-package +cd my-package/ +meteor npm init +git add -f ./ # or use a git submodule +``` + +### Using npm link + +Place the package outside your app's directory in a separate repository and use [`npm link`](https://docs.npmjs.com/cli/link). Do this when you want to use the package in multiple apps: + +```bash +cd ~/ +mkdir my-package +cd my-package/ +meteor npm init + +# Register the package globally +npm link + +# Link it in your app +cd ~/my-app/ +meteor npm link my-package +``` + +Other developers will also need to run the `npm link` command. + +### Using npm workspaces + +For monorepo setups, npm workspaces provide a cleaner solution: + +```json +// root package.json +{ + "name": "my-monorepo", + "workspaces": [ + "packages/*", + "apps/*" + ] +} +``` + +Then organize your packages: + +``` +my-monorepo/ +├── package.json +├── packages/ +│ └── my-package/ +│ └── package.json +└── apps/ + └── my-meteor-app/ + └── package.json +``` + +After any method, edit the `dependencies` attribute of your app's `package.json`, adding `"my-package": "1.0.0"` (use the same version number you chose during `meteor npm init`). + +## Writing Meteor-compatible packages + +When writing npm packages that will be used in Meteor, keep these considerations in mind: + +### Async/await patterns + +Meteor 3 uses async/await throughout. Your package should follow the same patterns: + +```js +// Good - async pattern +export async function getUserData(userId) { + const response = await fetch(`/api/users/${userId}`); + return response.json(); +} + +// Export both sync and async versions if needed +export function getUserDataSync(userId) { + // sync implementation for client-side use +} + +export async function getUserDataAsync(userId) { + // async implementation +} +``` + +### Environment detection + +Use Meteor's environment detection when available: + +```js +export function doSomething() { + if (typeof Meteor !== 'undefined') { + // Running in Meteor + if (Meteor.isServer) { + // Server-side code + } else if (Meteor.isClient) { + // Client-side code + } + } else { + // Running outside Meteor (tests, Node.js, etc.) + } +} +``` + +### Isomorphic packages + +For packages that work on both client and server: + +```js +// utils.js - works everywhere +export function formatDate(date) { + return new Intl.DateTimeFormat('en-US').format(date); +} + +// server.js - server only +export async function queryDatabase(query) { + // Database operations +} + +// client.js - client only +export function showNotification(message) { + // UI notifications +} +``` + +Then set up conditional exports: + +```json +{ + "name": "my-isomorphic-package", + "exports": { + ".": "./utils.js", + "./server": "./server.js", + "./client": "./client.js" + } +} +``` + +## Publishing your package + +You can share your package with others by publishing it to the npm registry. While most packages are public, you can control who may view and use your package with [private modules](https://docs.npmjs.com/private-modules/intro). + +### Public packages + +To publish publicly: + +1. Create an npm account at [npmjs.com](https://www.npmjs.com/) +2. Log in from the command line: + ```bash + npm login + ``` +3. Publish your package: + ```bash + npm publish + ``` + +When you're done, anyone can add your package to their app with `npm install --save your-package`. + +### Scoped packages + +For organization packages, use scoped names: + +```bash +npm init --scope=@myorg +npm publish --access public +``` + +Users install with: + +```bash +npm install @myorg/my-package +``` + +### Development workflow + +If you want to share packages during development, we recommend using `npm link` or workspaces instead of the registry. If you use the registry, then every time you change the package, you need to: + +1. Increment the version number +2. Publish +3. Run `npm update my-package` inside your app + +## Overriding packages with a local version + +If you need to modify a package to do something that the published version doesn't do, you can edit a local version of the package on your computer. + +Let's say you want to modify an npm package. First, install it if you haven't already: + +```bash +meteor npm install --save some-package +``` + +Now the code has been downloaded to `node_modules/some-package/`. Add the directory to source control: + +```bash +git add -f node_modules/some-package/ +``` + +Now you can edit the package, commit, and push, and your teammates will get your version of the package. + +To ensure that your package doesn't get overwritten during an `npm update`, change the default caret version range in your `package.json` to an exact version. + +Before: + +```json +"some-package": "^1.0.2", +``` + +After: + +```json +"some-package": "1.0.2", +``` + +### Using a Git repository + +An alternative method is maintaining a separate repository for the package and changing the `package.json` version to a git URL: + +```json +{ + "dependencies": { + "some-package": "git+https://github.com/yourusername/some-package.git#main" + } +} +``` + +Or use a specific commit: + +```json +{ + "dependencies": { + "some-package": "git+https://github.com/yourusername/some-package.git#abc1234" + } +} +``` + +Every time you edit the separate repo, you'll need to commit, push, and run `npm update some-package`. + +## TypeScript support + +To add TypeScript support to your npm package: + +### Installing dependencies + +```bash +npm install --save-dev typescript +``` + +### Configuration + +Create a `tsconfig.json`: + +```json +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "node", + "declaration": true, + "declarationMap": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} +``` + +### Package.json setup + +```json +{ + "name": "my-typescript-package", + "version": "1.0.0", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "scripts": { + "build": "tsc", + "prepublishOnly": "npm run build" + } +} +``` + +### Source code + +```typescript +// src/index.ts +export interface UserData { + id: string; + name: string; + email: string; +} + +export async function fetchUser(id: string): Promise { + const response = await fetch(`/api/users/${id}`); + return response.json(); +} +``` + +## Testing your package + +Set up testing for your npm package: + +### Jest configuration + +```bash +npm install --save-dev jest @types/jest ts-jest +``` + +Create `jest.config.js`: + +```js +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + testMatch: ['**/*.test.ts'], +}; +``` + +### Writing tests + +```typescript +// src/index.test.ts +import { formatDate } from './index'; + +describe('formatDate', () => { + it('should format dates correctly', () => { + const date = new Date('2024-01-15'); + expect(formatDate(date)).toMatch(/1\/15\/2024/); + }); +}); +``` + +### Package.json scripts + +```json +{ + "scripts": { + "test": "jest", + "test:watch": "jest --watch" + } +} +``` + +## Best practices + +1. **Use semantic versioning** - Follow [semver](https://semver.org/) for version numbers. +2. **Include a README** - Document your package's API and usage. +3. **Add a LICENSE file** - Specify the license for your package. +4. **Use .npmignore** - Exclude development files from the published package. +5. **Test before publishing** - Run your test suite before every publish. +6. **Use lockfiles** - Commit `package-lock.json` for reproducible builds. +7. **Keep dependencies minimal** - Only include necessary dependencies. +8. **Support tree-shaking** - Use ES modules to enable tree-shaking in bundlers. diff --git a/v3-docs/docs/packages/2.using-atmosphere-packages.md b/v3-docs/docs/packages/6.using-atmosphere-packages.md similarity index 51% rename from v3-docs/docs/packages/2.using-atmosphere-packages.md rename to v3-docs/docs/packages/6.using-atmosphere-packages.md index dbdc63daaf..91ca108a34 100644 --- a/v3-docs/docs/packages/2.using-atmosphere-packages.md +++ b/v3-docs/docs/packages/6.using-atmosphere-packages.md @@ -1,18 +1,34 @@ +# Using Atmosphere Packages + +Atmosphere packages are Meteor-specific packages published to [Atmosphere](https://atmospherejs.com). This guide covers how to find, install, and use Atmosphere packages in your Meteor application. + +## Searching for packages + +There are a few ways to search for Meteor packages published to Atmosphere: + +1. Search on the [Atmosphere website](https://atmospherejs.com/) +2. Use `meteor search` from the command line +3. Use a community package search website like [Fastosphere](http://fastosphere.meteor.com/) + +The main Atmosphere website provides additional curation features like trending packages, package stars, and flags, but some of the other options can be faster if you're trying to find a specific package. For example, you can use `meteor show ostrio:flow-router-extra` from the command line to see the description of that package and different available versions. + ### Package naming -All packages on Atmosphere have a name of the form `prefix:package-name`. The prefix is the Meteor Developer username of the organization or user that published the package. Meteor uses such a convention for package naming to make sure that it's clear who has published a certain package, and to avoid an ad-hoc namespacing convention. Meteor platform packages do not have any `prefix:` or may use `mdg:`. +You may notice that, with the exception of Meteor platform packages, all packages on Atmosphere have a name of the form `prefix:package-name`. The prefix is the Meteor Developer username of the organization or user that published the package. Meteor uses such a convention for package naming to make sure that it's clear who has published a certain package, and to avoid an ad-hoc namespacing convention. Meteor platform packages do not have any `prefix:`. -### Installing Atmosphere Packages +## Installing Atmosphere packages -To install an Atmosphere package, you use `meteor add` inside your app directory: +To install an Atmosphere package, you run `meteor add`: ```bash meteor add ostrio:flow-router-extra ``` -This will add the newest version of the desired package that is compatible with the other packages in your app and your Meteor app version. If you want to specify a particular version, you can specify it by adding a suffix to the package name like: `meteor add ostrio:flow-router-extra@3.12.0`. +This will add the newest version of the desired package that is compatible with the other packages in your app. If you want to specify a particular version, you can specify it by adding a suffix to the package name like: `meteor add ostrio:flow-router-extra@3.5.0`. -Regardless of how you add the package to your app, its actual version will be tracked in the file at `.meteor/versions`. This means that anybody collaborating with you on the same app is guaranteed to have the same package versions as you. If you want to update to a newer version of a package after installing it, use `meteor update`. You can run `meteor update` without any arguments to update all packages and Meteor itself to their latest versions, or pass a specific package to update just that one, for example `meteor update ostrio:flow-router-extra`. +Regardless of how you add the package to your app, its actual version will be tracked in the file at `.meteor/versions`. This means that anybody collaborating with you on the same app is guaranteed to have the same package versions as you. + +If you want to update to a newer version of a package after installing it, use `meteor update`. You can run `meteor update` without any arguments to update all packages and Meteor itself to their latest versions, or pass a specific package to update just that one, for example `meteor update ostrio:flow-router-extra`. If your app is running when you add a new package, Meteor will automatically download it and restart your app for you. @@ -30,25 +46,25 @@ To remove an unwanted Atmosphere package run: meteor remove ostrio:flow-router-extra ``` -You can get more details on all the package commands in the [Meteor Command line documentation](https://docs.meteor.com/cli/#meteorhelp). +You can get more details on all the package commands in the [Meteor Command line documentation](/cli/). -### Using Atmosphere Packages inside your app +## Using Atmosphere packages To use an Atmosphere Package in your app you can import it with the `meteor/` prefix: ```js -import { Mongo } from "meteor/mongo"; +import { Mongo } from 'meteor/mongo'; ``` -Typically a package will export one or more symbols, which you'll need to reference with the destructuring syntax. You can find these exported symbols by either looking in that package's `package.js` file for [`api.export`](http://docs.meteor.com/api/package.html#PackageAPI-export) calls or by looking in that package's main JavaScript file for ES2015 `export ` calls like `export const packageName = 'package-name';`. +Typically a package will export one or more symbols, which you'll need to reference with the destructuring syntax. You can find these exported symbols by either looking in that package's `package.js` file for `api.export` calls or by looking in that package's main JavaScript file for ES2015 `export` calls like `export const packageName = 'package-name';`. Sometimes a package will have no exports and have side effects when included in your app. In such cases you don't need to import the package at all after installing. -> For backwards compatibility with Meteor 1.2 and early releases, Meteor by default makes available directly to your app all symbols referenced in `api.export` in any packages you have installed. However, it is recommended that you import these symbols first before using them. +> For backwards compatibility with older Meteor releases, Meteor by default makes available directly to your app all symbols referenced in `api.export` in any packages you have installed. However, it is recommended that you import these symbols first before using them. -#### Importing styles from Atmosphere packages +### Importing styles from Atmosphere packages -Using any of Meteor's supported CSS pre-processors you can import other style files using the `{package-name}` syntax as long as those files are designated to be lazily evaluated as "import" files. To get more details on how to determine this see [CSS source versus import](https://guide.meteor.com/build-tool#css-source-vs-import) files. +Using any of Meteor's supported CSS pre-processors you can import other style files using the `{package-name}` syntax as long as those files are designated to be lazily evaluated as "import" files. To get more details on how to determine this see the [Build Tool article](/about/build-tool#css-source-vs-import). ```less @import '{prefix:package-name}/buttons/styles.import.less'; @@ -56,18 +72,18 @@ Using any of Meteor's supported CSS pre-processors you can import other style fi > CSS files in an Atmosphere package are declared with `api.addFiles`, and therefore will be eagerly evaluated by default, and then bundled with all the other CSS in your app. -#### Peer npm dependencies +### Peer npm dependencies -Atmosphere packages can ship with contained [npm dependencies](#npm-dependencies), in which case you don't need to do anything to make them work. However, some Atmosphere packages will expect that you have installed certain "peer" npm dependencies in your application. +Atmosphere packages can ship with contained npm dependencies, in which case you don't need to do anything to make them work. However, some Atmosphere packages will expect that you have installed certain "peer" npm dependencies in your application. -Typically the package will warn you if you have not done so. For example, if you install the [`react-meteor-data`](https://atmospherejs.com/meteor/react-meteor-data) package into your app, you'll also need to install the [`react`](https://www.npmjs.com/package/react) and the [`react-addons-pure-render-mixin`](https://www.npmjs.com/package/react-addons-pure-render-mixin) packages: +Typically the package will warn you if you have not done so. For example, if you install the `react-meteor-data` package into your app, you'll also need to install the `react` package: ```bash -meteor npm install --save react react-addons-pure-render-mixin +meteor npm install --save react meteor add react-meteor-data ``` -### Atmosphere package namespacing +## Atmosphere package namespacing Each Atmosphere package that you use in your app exists in its own separate namespace, meaning that it sees only its own global variables and any variables provided by the packages that it specifically uses. When a top-level variable is defined in a package, it is either declared with local scope or package scope. @@ -76,19 +92,23 @@ Each Atmosphere package that you use in your app exists in its own separate name * local scope - this variable is not visible outside of the block it is * declared in and other packages and your app won't see it */ -const alicePerson = {name: "alice"}; +const alicePerson = { name: 'alice' }; /** * package scope - this variable is visible to every file inside of the * package where it is declared and to your app */ -bobPerson = {name: "bob"}; +bobPerson = { name: 'bob' }; ``` Notice that this is just the normal JavaScript syntax for declaring a variable that is local or global. Meteor scans your source code for global variable assignments and generates a wrapper that makes sure that your globals don't escape their appropriate namespace. -In addition to local scope and package scope, there are also package exports. A package export is a "pseudo global" variable that a package makes available for you to use when you install that package. For example, the `email` package exports the `Email` variable. If your app uses the `email` package (and _only_ if it uses the `email` package!) then your app can access the `Email` symbol and you can call `Email.send`. Most packages have only one export, but some packages might have two or three (for example, a package that provides several classes that work together). +In addition to local scope and package scope, there are also package exports. A package export is a "pseudo global" variable that a package makes available for you to use when you install that package. For example, the `email` package exports the `Email` variable. If your app uses the `email` package (and *only* if it uses the `email` package!) then your app can access the `Email` symbol and you can call `Email.send`. > It is recommended that you use the `ecmascript` package and first call `import { Email } from 'meteor/email';` before calling `Email.send` in your app. It is also recommended that package developers now use ES2015 `export` from their main JavaScript file instead of `api.export`. Your app sees only the exports of the packages that you use directly. If you use package A, and package A uses package B, then you only see package A's exports. Package B's exports don't "leak" into your namespace just because you used package A. Each app or package only sees their own globals plus the APIs of the packages that they specifically use and depend upon. + +## Atmosphere vs. npm + +For a detailed comparison of when to use Atmosphere packages vs. npm packages, see the [Meteor Package System overview](/packages/#atmosphere-vs-npm). diff --git a/v3-docs/docs/packages/7.writing-atmosphere-packages.md b/v3-docs/docs/packages/7.writing-atmosphere-packages.md new file mode 100644 index 0000000000..69a8b1dc67 --- /dev/null +++ b/v3-docs/docs/packages/7.writing-atmosphere-packages.md @@ -0,0 +1,478 @@ +# Writing Atmosphere Packages + +This guide covers how to create and publish packages to [Atmosphere](https://atmospherejs.com), Meteor's package repository. + +## Getting started + +To get started writing a package, use the Meteor command line tool: + +```bash +meteor create --package my-package +``` + +> It is required that your `my-package` name take the form of `username:my-package`, where `username` is your Meteor Developer username, if you plan to publish your package to Atmosphere. + +If you run this inside an app, it will place the newly generated package in that app's `packages/` directory. Outside an app, it will just create a standalone package directory. The command also generates some boilerplate files for you: + +``` +my-package +├── README.md +├── package.js +├── my-package-tests.js +└── my-package.js +``` + +The `package.js` file is the main file in every Meteor package. This is a JavaScript file that defines the metadata, files loaded, architectures, npm packages, and Cordova packages for your Meteor package. + +In this guide, we will go over some important points for building packages, but we won't explain every part of the `package.js` API. To learn about all of the options, [read about the Package.js API](/api/package). + +> Don't forget to run `meteor add [my-package]` once you have finished developing your package in order to use it; this applies if the package is a local package for internal use only or if you have published the package to Atmosphere. + +## Adding files and assets + +The main function of an Atmosphere package is to contain source code (JS, CSS, and any transpiled languages) and assets (images, fonts, and more) that will be shared across different applications. + +### Adding JavaScript + +To add JavaScript files to a package, specify an entrypoint with `api.mainModule()` in the package's `onUse` block (this will already have been done by `meteor create --package` above): + +```js +Package.onUse(function(api) { + api.mainModule('my-package.js'); +}); +``` + +From that entrypoint, you can `import` other files within your package, just as you would in an application. + +If you want to include different files on the client and server, you can specify multiple entry points using the second argument to the function: + +```js +Package.onUse(function(api) { + api.mainModule('my-package-client.js', 'client'); + api.mainModule('my-package-server.js', 'server'); +}); +``` + +You can also add any source file that would be compiled to a JS file (such as a CoffeeScript file) in a similar way, assuming you depend on an appropriate build plugin. + +### Adding CSS + +To include CSS files with your package you can use `api.addFiles()`: + +```js +Package.onUse(function(api) { + api.addFiles('my-package.css', 'client'); +}); +``` + +The CSS file will be automatically loaded into any app that uses your package. + +### Adding Sass, Less, or Stylus mixins/variables + +Just like packages can export JavaScript code, they can export reusable bits of CSS pre-processor code. You can also have a package that doesn't actually include any CSS, but just exports different bits of reusable mixins and variables. + +```js +Package.onUse(function(api) { + api.addFiles('my-package.scss', 'client'); +}); +``` + +This Sass file will be eagerly evaluated and its compiled form will be added to the CSS of the app immediately. + +```js +Package.onUse(function(api) { + api.addFiles([ + 'stylesheets/_util.scss', + 'stylesheets/_variables.scss' + ], 'client', { isImport: true }); +}); +``` + +These two Sass files will be lazily evaluated and only included in the CSS of the app if imported from some other file. + +### Adding other assets + +You can include other assets, such as fonts, icons or images, to your package using `api.addAssets`: + +```js +Package.onUse(function(api) { + api.addAssets([ + 'font/OpenSans-Regular-webfont.eot', + 'font/OpenSans-Regular-webfont.svg', + 'font/OpenSans-Regular-webfont.ttf', + 'font/OpenSans-Regular-webfont.woff', + ], 'client'); +}); +``` + +You can then access these files from the client from a URL `/packages/username_my-package/font/OpenSans-Regular-webfont.eot` or from the server using the [Assets API](/api/assets). + +## Exporting + +While some packages exist just to provide side effects to the app, most packages provide a reusable bit of code that can be used by the consumer with `import`. To export a symbol from your package, use the ES2015 `export` syntax in your `mainModule`: + +```js +// in my-package.js: +export const myName = 'my-package'; +``` + +Now users of your package can import the symbol with: + +```js +import { myName } from 'meteor/username:my-package'; +``` + +## Development-Only Packages (Meteor 3.4+) + +**New in Meteor 3.4** ([PR#13797](https://github.com/meteor/meteor/pull/13797)): You can mark packages as development-only, which means they will be excluded from production builds. This is useful for debugging tools, development utilities, testing frameworks, and other packages that should only run during development. + +### Using `devOnly` + +To mark a package as development-only, set the `devOnly` option in `Package.describe()`: + +```js +Package.describe({ + name: 'username:dev-tools', + version: '1.0.0', + summary: 'Development debugging tools', + devOnly: true // This package will be excluded from production builds +}); + +Package.onUse(function(api) { + api.versionsFrom('3.4'); + api.use('ecmascript'); + api.mainModule('dev-tools.js', 'server'); +}); +``` + +**Common use cases for `devOnly` packages:** +- Development debugging tools and loggers +- In-app development consoles +- Mock data generators +- Development-only admin panels +- Testing utilities +- Performance profiling tools + +**Benefits:** +- **Smaller Production Bundles**: Development tools don't ship to production +- **Faster Production Builds**: Less code to process and minify +- **Clear Separation**: Explicit distinction between dev and production dependencies + +**Example - Development Logger Package:** + +```js +// packages/username:dev-logger/package.js +Package.describe({ + name: 'username:dev-logger', + version: '1.0.0', + summary: 'Enhanced logging for development', + devOnly: true +}); + +Package.onUse(function(api) { + api.versionsFrom('3.4'); + api.use(['ecmascript', 'mongo']); + api.mainModule('dev-logger.js', 'server'); +}); +``` + +```js +// packages/username:dev-logger/dev-logger.js +import { Meteor } from 'meteor/meteor'; + +export const DevLogger = { + logQuery(collection, query) { + console.log('[DEV]', collection._name, 'query:', JSON.stringify(query, null, 2)); + }, + + logMethodCall(name, args) { + console.log('[DEV] Method called:', name, 'with args:', args); + } +}; + +// This entire package and its code will not be included in production builds +``` + +## Dependencies + +Chances are your package will want to make use of other packages. To ensure they are available, you can declare dependencies. Atmosphere packages can depend both on other Atmosphere packages, as well as packages from npm. + +### Atmosphere dependencies + +To depend on another Atmosphere package, use `api.use`: + +```js +Package.onUse(function(api) { + // This package depends on 1.2.0 or above of validated-method + api.use('mdg:validated-method@1.2.0'); +}); +``` + +One important feature of the Atmosphere package system is that it is single-loading: no two packages in the same app can have dependencies on conflicting versions of a single package. + +### Depending on Meteor version + +Note that the Meteor release version number is mostly a marketing artifact - the core Meteor packages themselves typically don't share this version number. This means packages can only depend on specific versions of the packages inside a Meteor release, but can't depend on a specific release itself. We have a helpful shorthand API called `api.versionsFrom` that handles this for you by automatically filling in package version numbers from a particular release: + +```js +Package.onUse(function(api) { + // Use versions of core packages from Meteor 3.0 + api.versionsFrom('3.0'); + + api.use([ + // Don't need to specify version because of versionsFrom above + 'ecmascript', + 'check', + + // Still need to specify versions of non-core packages + 'mdg:validated-method@1.2.0', + 'mdg:validation-error@0.1.0' + ]); +}); +``` + +Additionally, you can call `api.versionsFrom()` multiple times, or with an array (e.g., `api.versionsFrom(['3.0', '2.14'])`). Meteor will interpret this to mean that the package will work with packages from all the listed releases. + +### Semantic versioning and version constraints + +Meteor's package system relies heavily on [Semantic Versioning](http://semver.org/), or SemVer. When one package declares a dependency on another, it always comes with a version constraint. These version constraints are then solved by Meteor's Version Solver to arrive at a set of package versions that meet all of the requirements. + +The mental model here is: + +1. **The major version must always match exactly.** If package `a` depends on `b@2.0.0`, the constraint will only be satisfied if the version of package `b` starts with a `2`. This means that you can never have two different major versions of a package in the same app. +2. **The minor and patch version numbers must be greater or equal to the requested version.** If the dependency requests version `2.1.3`, then `2.1.4` and `2.2.0` will work, but `2.0.4` and `2.1.2` will not. + +If your package supports multiple major versions of a dependency, you can supply both versions to `api.use` like so: + +```js +api.use('blaze@1.0.0 || 2.0.0'); +``` + +### npm dependencies + +Meteor packages can include npm packages to use JavaScript code from outside the Meteor package ecosystem or to include JavaScript code with native dependencies. Use `Npm.depends` at the top level of your `package.js` file: + +```js +Npm.depends({ + 'github': '0.2.4' +}); +``` + +If you want to use a local npm package, for example during development, you can give a directory instead of a version number: + +```js +Npm.depends({ + 'my-package': 'file:///home/user/npms/my-package' +}); +``` + +You can import the dependency from within your package code in the same way that you would inside an application: + +```js +import github from 'github'; +``` + +### Development-only npm dependencies (Meteor 3.4+) + +**New in Meteor 3.4** ([PR#13797](https://github.com/meteor/meteor/pull/13797)): Use `Npm.devDepends()` to declare npm dependencies that are only needed during development. These dependencies will be excluded from production builds, reducing your production bundle size. + +```js +// Production dependencies - always included +Npm.depends({ + 'lodash': '4.17.21', + 'moment': '2.29.4' +}); + +// Development-only dependencies - excluded from production +Npm.devDepends({ + 'debug-tool': '2.0.0', + 'dev-helper': '1.5.0', + 'mock-data-generator': '3.1.0' +}); +``` + +**When to use `Npm.devDepends()`:** +- Development debugging libraries +- Mock data generators +- Development server utilities +- Testing helpers that aren't used in production +- Development-only logging frameworks + +**Benefits:** +- **Smaller Production Bundles**: Dev dependencies don't ship to production +- **Faster Production Builds**: Less code to transpile and minify +- **Clear Dependency Management**: Explicit separation between production and development dependencies + +**Example - Complete Package with Dev Dependencies:** + +```js +// packages/username:data-layer/package.js +Package.describe({ + name: 'username:data-layer', + version: '1.0.0', + summary: 'Data access layer with development tools' +}); + +// Production dependencies +Npm.depends({ + 'lodash': '4.17.21', + 'validator': '13.7.0' +}); + +// Development-only dependencies +Npm.devDepends({ + 'faker': '5.5.3', // For generating test data + 'debug': '4.3.4', // For development logging + 'pretty-format': '27.5.1' // For formatting debug output +}); + +Package.onUse(function(api) { + api.versionsFrom('3.4'); + api.use(['ecmascript', 'mongo']); + api.mainModule('data-layer.js'); +}); +``` + +```js +// packages/username:data-layer/data-layer.js +import { Meteor } from 'meteor/meteor'; +import _ from 'lodash'; +import validator from 'validator'; + +// These imports will only work in development +let faker, debug, prettyFormat; +if (Meteor.isDevelopment) { + faker = require('faker'); + debug = require('debug')('data-layer'); + prettyFormat = require('pretty-format'); +} + +export const DataLayer = { + validateEmail(email) { + return validator.isEmail(email); + }, + + // Development-only method + generateTestUsers(count) { + if (!Meteor.isDevelopment) { + throw new Meteor.Error('not-available', 'Only available in development'); + } + + return _.times(count, () => ({ + name: faker.name.findName(), + email: faker.internet.email(), + createdAt: new Date() + })); + }, + + // Development-only debugging + debugQuery(query) { + if (Meteor.isDevelopment && debug) { + debug('Query:', prettyFormat(query)); + } + } +}; +``` + +**Important Notes:** +- `Npm.devDepends()` dependencies are available during development via `require()` or dynamic `import()` +- Always check `Meteor.isDevelopment` before using dev dependencies in your code +- Dev dependencies are still installed in `node_modules` but excluded from production builds +- Using a dev dependency in production code without the development check will cause runtime errors in production + +### Peer npm dependencies + +`Npm.depends()` is fairly rigid (you can only depend on an exact version), and will typically result in multiple versions of a package being installed if many different Atmosphere packages depend on the same npm package. This makes it less than ideal to use on the client, where it's impractical to ship multiple copies of the same package code to the browser. + +To avoid this problem as a package author, you can request that users of your package have installed the npm package you want to use at the application level. This is similar to a peer dependency of an npm package. You can use the `tmeasday:check-npm-versions` package to ensure that they've done this, and to warn them if not. + +For instance, if you are writing a React package, you should not directly depend on `react`, but instead use `check-npm-versions` to check the user has installed it: + +```js +import { checkNpmVersions } from 'meteor/tmeasday:check-npm-versions'; + +checkNpmVersions({ + 'react': '18.x' +}, 'my:awesome-package'); + +// If you are using the dependency in the same file, you'll need to use require +const React = require('react'); +``` + +## Cordova plugins + +Atmosphere packages can include Cordova plugins to ship native code for the Meteor mobile app container. This way, you can interact with the native camera interface, use the gyroscope, save files locally, and more. + +Include Cordova plugins in your Meteor package by using `Cordova.depends`. + +Read more about using Cordova in the [mobile guide](/about/cordova). + +## Testing packages + +Meteor has a test mode for packages invoked with the `meteor test-packages` command. Navigate to your package's directory and then use the command to run a special app containing only a "test" version of your package. + +If you are using Tinytest for your package's tests, you can run: + +```bash +meteor test-packages ./ +``` + +If you are using a different testing framework for your package's tests, you'll need to specify a `driver-package`. For example, if you are using Mocha: + +```bash +meteor test-packages ./ --driver-package meteortesting:mocha +``` + +When your package starts in test mode, rather than loading the `onUse` block, Meteor loads the `onTest` block: + +```js +Package.onTest(function(api) { + // You almost definitely want to depend on the package itself, + // this is what you are testing! + api.use('my-package'); + + // You should also include any packages you need to use in the test code + api.use(['ecmascript', 'random', 'meteortesting:mocha']); + + // Finally add an entry point for tests + api.mainModule('my-package-tests.js'); +}); +``` + +From within your test entry point, you can import other files as you would in the package proper. + +You can also use [`mtest`](https://github.com/zodern/mtest) to test your packages: + +```bash +mtest --package ./ --once 3.0 +``` + +This helps immensely if you'd like to test your package in CI/CD setup. + +You can read more about testing in Meteor in the [Testing article](/tutorials/testing/testing). + +## Publishing your package + +To publish your package to Atmosphere for the first time, run `meteor publish --create` from the package directory. The package name must follow the format of `username:my-package` and the package must contain a SemVer version number. When you want to publish an update to your package, change the version number in `package.js` and then run `meteor publish`. + +> Note that if you have a local `node_modules` directory in your package, remove it before running `meteor publish`. While local `node_modules` directories are allowed in Meteor packages, their paths can collide with the paths of `Npm.depends` dependencies when published. + +### Cache format + +If you've ever looked inside Meteor's package cache at `~/.meteor/packages`, you know that the on-disk format of a built Meteor package is completely different from the way the source code looks when you're developing the package. The idea is that the target format of a package can remain consistent even if the API for development changes. + +## Local packages + +As an alternative to publishing your package on Atmosphere, if you want to keep your package private, you can place it in your Meteor app in the `packages/` directory, for instance `packages/foo/`, and then add it to your app with `meteor add foo`. + +### Overriding published packages with a local version + +If you need to modify an Atmosphere package to do something that the published version doesn't do, you can edit a local version of the package on your computer. + +A Meteor app can load Atmosphere packages in one of three ways, and it looks for a matching package name in the following order: + +1. Package source code in the `packages/` directory inside your app. +2. Package source code in directories indicated by setting a `METEOR_PACKAGE_DIRS` environment variable before running any `meteor` command. You can add multiple directories by separating the paths with a `:` on OSX or Linux, or a `;` on Windows. For example: `METEOR_PACKAGE_DIRS=../first/directory:../second/directory`. +3. Pre-built package from Atmosphere. The package is cached in `~/.meteor/packages` on Mac/Linux or `%LOCALAPPDATA%\.meteor\packages` on Windows, and only loaded into your app as it is built. + +You can use (1) or (2) to override the version from Atmosphere. You can even do this to load patched versions of Meteor core packages - just copy the code of the package from [Meteor's GitHub repository](https://github.com/meteor/meteor/tree/devel/packages), and edit away. diff --git a/v3-docs/docs/packages/accounts-ui.md b/v3-docs/docs/packages/accounts-ui.md index e4f7d341ed..6de1babd61 100644 --- a/v3-docs/docs/packages/accounts-ui.md +++ b/v3-docs/docs/packages/accounts-ui.md @@ -28,7 +28,7 @@ when the URLs are loaded. ## Customizing the UI -If you want to control the look and feel of your accounts system a little more, we recommend reading the [useraccounts](http://guide.meteor.com/accounts.html#useraccounts) section of the Meteor Guide. +If you want to control the look and feel of your accounts system a little more, we recommend reading the [useraccounts](/tutorials/accounts/accounts#useraccounts) section of the Meteor Guide. ### CSS Variables diff --git a/v3-docs/docs/packages/autoupdate.md b/v3-docs/docs/packages/autoupdate.md index a54575e506..2e01d27a3b 100644 --- a/v3-docs/docs/packages/autoupdate.md +++ b/v3-docs/docs/packages/autoupdate.md @@ -34,6 +34,26 @@ update will take place: Meteor will force a complete browser reload using the There is no soft update with Cordova apps, the client is always fully refreshed once a change is detected. +### Modern Architecture Support (Meteor 3.4) + +**web.cordova as Modern Architecture** ([PR#13983](https://github.com/meteor/meteor/pull/13983)) + +Starting with Meteor 3.4, `web.cordova` is treated as a modern architecture, bringing several benefits: + +- **Smaller Bundle Sizes**: Modern JavaScript features are preserved, reducing transpilation overhead +- **Better Performance**: Native modern browser features in Cordova WebView +- **Consistent Behavior**: Aligns with how modern web browsers are treated + +This change means Cordova apps now benefit from the same modern build optimizations as web browsers, resulting in faster load times and smaller bundle sizes. + +To leverage this in your existing Cordova apps, ensure you're using Meteor 3.4+ and your `mobile-config.js` targets modern WebView versions: + +```js +// mobile-config.js +App.setPreference('android-minSdkVersion', '23'); // Android 6.0+ +App.setPreference('ios-deployment-target', '12.0'); // iOS 12.0+ +``` + ### `usesCleartextTraffic` Starting with Android 9 (API level 28), [cleartext support is disabled](https://developer.android.com/training/articles/security-config) by default. During development `autoupdate` uses cleartext to publish new client versions. @@ -59,4 +79,4 @@ should be on the same network, and you should run your app with `meteor run andr where *XXX.XXX.XXX.XXX* is your local development address, _e.g. 192.168.1.4_. > To have a better understanding of how HCP works for mobile apps already -> published to production refer to [Hot code push on mobile](https://guide.meteor.com/cordova.html#hot-code-push) +> published to production refer to [Hot code push on mobile](/troubleshooting/hot-code-push) diff --git a/v3-docs/docs/packages/index.md b/v3-docs/docs/packages/index.md index 46a951120d..3549360fe6 100644 --- a/v3-docs/docs/packages/index.md +++ b/v3-docs/docs/packages/index.md @@ -1,18 +1,47 @@ -# Meteor packaging system: Atmosphere packages +# Meteor Package System -In this tutorial we will present the Meteor packaging system: Atmosphere packages. Atmosphere packages are a way to create reusable code that can shared by multiple apps. They can contain both client and server code, as well as assets like images and stylesheets. +Meteor supports two package ecosystems: **Atmosphere packages** built specifically for Meteor, and standard **npm packages**. You can use both in the same app. -Of course, you can also use npm packages in your Meteor apps. -This tutorial only focuses on Meteor packages, which are a different system. +## Atmosphere vs. npm + +With full npm support since Meteor 1.3, you may wonder when to use Atmosphere packages vs npm packages. + +**When to use Atmosphere packages:** + +Atmosphere packages are written specifically for Meteor and have several advantages over npm when used with Meteor. In particular, Atmosphere packages can: + +- Depend on core Meteor packages, such as `ddp`, `mongo`, or `accounts` +- Explicitly include non-JavaScript files including CSS, Less, Sass, Stylus, and static assets +- Take advantage of Meteor's [build system](/about/build-tool) to be automatically transpiled from languages like CoffeeScript +- Have a well-defined way to ship different code for client and server, enabling different behavior in each context +- Get direct access to Meteor's package namespacing and package global exports without having to explicitly use ES2015 `import` +- Enforce exact version dependencies between packages using Meteor's constraint resolver +- Include [build plugins](/api/package#build-plugin-api) for Meteor's build system +- Include pre-built binary code for different server architectures, such as Linux or Windows + +If your package depends on another Atmosphere package, or needs to take advantage of Meteor's [build system](/about/build-tool), writing an Atmosphere package might be the best option. + +For more details, see [Using Atmosphere Packages](/packages/6.using-atmosphere-packages) and [Writing Atmosphere Packages](/packages/7.writing-atmosphere-packages). + +**When to use npm packages:** + +npm is a repository of general JavaScript packages. Today, npm is used for all types of JavaScript packages across client and server environments. + +If you want to distribute and reuse code that you've written for a Meteor application, consider publishing that code on npm if it's general enough to be consumed by a wider JavaScript audience. It's possible to use npm packages in Meteor applications, and possible to use npm packages within Atmosphere packages, so even if your main audience is Meteor developers, npm might be the best choice. + +Meteor comes with npm bundled so that you can type `meteor npm` without worrying about installing it yourself. You can use `meteor npm` in the same way you would use a globally installed npm. + +For more details, see [Using npm Packages](/packages/4.using-npm-packages) and [Writing npm Packages](/packages/5.writing-npm-packages). + +## Atmosphere package repositories Two public repositories of Meteor packages exist: - [Atmosphere](https://atmospherejs.com/): The original repository for Meteor packages, which hosts a wide variety of community-contributed packages. -- [Packosphere](https://packosphere.com/): A newer repository and community maintained alternative to Atmosphere. Packosphere has more information about package quality and maintenance status. +- [Packosphere](https://packosphere.com/): A newer repository and community-maintained alternative to Atmosphere. Packosphere has more information about package quality and maintenance status. # Table of Contents [[toc]] - diff --git a/v3-docs/docs/packages/modules.md b/v3-docs/docs/packages/modules.md index 67137c0fd7..ea34513f8c 100644 --- a/v3-docs/docs/packages/modules.md +++ b/v3-docs/docs/packages/modules.md @@ -161,7 +161,7 @@ if (Meteor.isClient) { } ``` -Note that dynamic calls to `require()` (where the name being required can change at runtime) cannot be analyzed correctly and may result in broken client bundles. This is also discussed in [the guide](http://guide.meteor.com/structure.html#using-require). +Note that dynamic calls to `require()` (where the name being required can change at runtime) cannot be analyzed correctly and may result in broken client bundles. This is also discussed in [the guide](/tutorials/application-structure/#using-require). ### CoffeeScript @@ -343,7 +343,7 @@ A version of the `npm` command comes bundled with every Meteor installation, and ## File load order -Before Meteor 1.3, the order in which application files were evaluated was dictated by a set of rules described in the [Application Structure - Default file load order](http://guide.meteor.com/structure.html#load-order) section of the Meteor Guide. These rules could become frustrating when one file depended on a variable defined by another file, particularly when the first file was evaluated after the second file. +Before Meteor 1.3, the order in which application files were evaluated was dictated by a set of rules described in the [Application Structure - Default file load order](/tutorials/application-structure/#load-order) section of the Meteor Guide. These rules could become frustrating when one file depended on a variable defined by another file, particularly when the first file was evaluated after the second file. Thanks to modules, any load-order dependency you might imagine can be resolved by adding an `import` statement. So if `a.js` loads before `b.js` because of their file names, but `a.js` needs something defined by `b.js`, then `a.js` can simply `import` that value from `b.js`: diff --git a/v3-docs/docs/packages/react-meteor-data.md b/v3-docs/docs/packages/react-meteor-data.md index 50717c82b1..b091fadfa6 100644 --- a/v3-docs/docs/packages/react-meteor-data.md +++ b/v3-docs/docs/packages/react-meteor-data.md @@ -193,7 +193,7 @@ export default withTracker(({ listId }) => { The returned component will, when rendered, render `Foo` (the "lower-order" component) with its provided props in addition to the result of the reactive function. So `Foo` will receive `{ listId }` (provided by its parent) as well as `{ currentUser, listLoading, tasks }` (added by the `withTracker` HOC). -For more information, see the [React article](http://guide.meteor.com/react.html) in the Meteor Guide. +For more information, see the [React tutorial](/tutorials/react/index). ### `withTracker({ reactiveFn, pure, skipUpdate })` diff --git a/v3-docs/docs/packages/webapp.md b/v3-docs/docs/packages/webapp.md index 09f4454e15..bd71bcbc22 100644 --- a/v3-docs/docs/packages/webapp.md +++ b/v3-docs/docs/packages/webapp.md @@ -29,7 +29,7 @@ WebApp.handlers.use("/hello", (req, res, next) => { One of the really cool things you can do with WebApp is serve static HTML for a landing page where TTFB (time to first byte) is of utmost importance. -The [Bundle Visualizer](https://docs.meteor.com/packages/bundle-visualizer.html) and [Dynamic Imports](https://docs.meteor.com/packages/dynamic-import.html) are great tools to help you minimize initial page load times. But sometimes you just need to skinny down your initial page load to bare metal. +The [Bundle Visualizer](/packages/bundle-visualizer) and [Dynamic Imports](/packages/dynamic-import) are great tools to help you minimize initial page load times. But sometimes you just need to skinny down your initial page load to bare metal. The good news is that WebApp makes this is really easy to do. @@ -103,6 +103,58 @@ We're using the [connect-route](https://www.npmjs.com/package/connect-route) NPM And finally, if you decide to use this technique you'll want to make sure you understand how conflicting client side routing will affect user experience. +### React SSR Optimization (Meteor 3.4) + +**Experimental: Disable Boilerplate Response** ([PR#13855](https://github.com/meteor/meteor/pull/13855)) + +Meteor 3.4 introduces an experimental configuration option to improve React Server-Side Rendering (SSR) performance by disabling the default boilerplate HTML response. + +When using React SSR with packages like `server-render`, you may want to completely control the HTML output without Meteor's default boilerplate injection. Enable this with: + +```js +// server/main.js +import { WebApp } from 'meteor/webapp'; + +WebApp.addHtmlAttributeHook(() => ({ + disableBoilerplateResponse: true +})); +``` + +**Benefits:** +- **Full SSR Control**: Complete control over the HTML output for SSR frameworks +- **Better Performance**: Reduces overhead by skipping boilerplate generation +- **Cleaner Output**: No automatic script/link injection, allowing custom placement + +**Use Cases:** +- Advanced React SSR implementations +- Custom HTML structure requirements +- Integration with SSR frameworks like Next.js patterns in Meteor + +**Important Notes:** +- This is an experimental feature and may change in future releases +- When enabled, you're responsible for including all necessary scripts and assets +- Best used with the `server-render` package for full SSR implementations + +Example with server-render: + +```js +import { onPageLoad } from 'meteor/server-render'; +import { renderToString } from 'react-dom/server'; +import { WebApp } from 'meteor/webapp'; + +WebApp.addHtmlAttributeHook(() => ({ + disableBoilerplateResponse: true +})); + +onPageLoad(sink => { + const html = renderToString(); + + sink.renderIntoElementById('root', html); + sink.appendToHead(''); + sink.appendToBody(''); +}); +``` + ### Dynamic Runtime Configuration In some cases it is valuable to be able to control the **meteor_runtime_config** variable that initializes Meteor at runtime. diff --git a/v3-docs/docs/performance/performance-improvement.md b/v3-docs/docs/performance/performance-improvement.md new file mode 100644 index 0000000000..5de6c40d38 --- /dev/null +++ b/v3-docs/docs/performance/performance-improvement.md @@ -0,0 +1,258 @@ +# Performance Improvements + +This guide focuses on providing you tips and common practices on how to improve performance of your Meteor app (sometimes also called scaling). + +After reading this guide, you'll know: + +1. How to use APM for performance monitoring +2. How to optimize publications and data loading +3. How to improve Method performance +4. MongoDB optimization strategies +5. Scaling approaches for growing applications + +It is important to note that at the end of the day Meteor is a Node.js app tied closely to MongoDB, so a lot of the problems you are going to encounter are common to other Node.js and MongoDB apps. Also do note that every app is different so there are unique challenges to each, therefore practices described in this guide should be used as guiding posts rather than absolutes. + +## Performance monitoring + +Before any optimization can take place we need to know what is our problem. This is where APM (Application Performance Monitor) comes in. + +The recommended Meteor-specific APM solution is [Monti APM](https://montiapm.com/). Unlike generic Node.js APM tools, Monti APM understands Meteor's DDP protocol, publications, methods, and livequery observers, giving you insights tailored to your Meteor app. + +> **Note:** Galaxy APM (`mdg:meteor-apm-agent`) has been discontinued. If you were using it, migrate to Monti APM which provides the same Meteor-specific monitoring capabilities. + +To get started, add the Monti APM agent to your Meteor app: + +```bash +meteor add montiapm:agent +``` + +Then configure it with your Monti APM credentials. See the [Monti APM documentation](https://docs.montiapm.com/) for full setup instructions, including [getting started](https://docs.montiapm.com/getting-started) and [dashboard guides](https://docs.montiapm.com/dashboards/jobs-dashboard). + +You can also choose other APM tools for Node.js (such as [Datadog](https://www.datadoghq.com/product/apm/) or [Elastic APM](https://github.com/Meteor-Community-Packages/meteor-elastic-apm)), but they will not show you Meteor-specific data like DDP response times, publication performance, and observer reuse metrics. + +### Finding issues in APM + +APM will start with providing you with an overview of how your app is performing. You can then dive deep into details of publications, methods, errors happening (both on client and server) and more. You will spend a lot of time in the detailed tabs looking for methods and publications to improve and analyzing the impact of your actions. + +The process, for example for optimizing methods, will look like this: + +1. Go to the detailed view under the Methods tab. +2. Sort the Methods Breakdown by Response Time. +3. Click on a method name in the Methods Breakdown. Assess the impact if you improve the selected method. +4. Look at the response time graph and find a trace. +5. Improve your method if you feel it is the right moment to do so. + +Not every long-performing method has to be improved. Take a look at the following example: + +- **methodX** - mean response time 1,515ms, throughput 100.05/min +- **methodY** - mean response time 34,000ms, throughput 0.03/min + +At first glance, the 34 seconds response time can catch your attention, and it may seem that the methodY is more relevant to improvement. But don't ignore the fact that this method is being used only once in a few hours by the system administrators or scheduled cron action. + +And now, let's take a look at the methodX. Its response time is evidently lower BUT compared to the frequency of use, it is still high, and without any doubt should be optimized first. + +It's also absolutely vital to remember that you shouldn't optimize everything as it goes. The key is to think strategically and match the most critical issues with your product priorities. + +## Publications + +Publications allow for the most prominent aspect of Meteor: live data. At the same this is the most resource intensive part of a Meteor application. + +Under the hood WebSockets are being used with additional abilities provided by DDP. + +### Proper use of publications + +Since publications can get resource intensive they should be reserved for usage that requires up-to-date, live data or that are changing frequently and you need the users to see that. + +You will need to evaluate your app to figure out which situations these are. As a rule of thumb any data that are not required to be live or are not changing frequently can be fetched once via other means and re-fetched as needed, in most cases the re-fetching shouldn't be necessary. + +But even before you proceed any further there are a few improvements that you can make here: + +- Only get the fields you need +- Limit the number of documents you send to the client (always set the `limit` option) +- Ensure that you have set all your indexes + +### Methods over publications + +The first easiest replacement is to use Meteor methods instead of publications. In this case you can use the existing publication and instead of returning a cursor you will call `.fetchAsync()` and return the actual data. The same performance improvements to get the method work faster apply here, but once called it sends the data and you don't have the overhead of a publication. + +```js +// Instead of a publication +Meteor.publish('allPosts', function() { + return Posts.find({}, { limit: 20 }); +}); + +// Use a method for one-time data loading +Meteor.methods({ + async getPosts() { + return await Posts.find({}, { limit: 20 }).fetchAsync(); + } +}); +``` + +What is crucial here is to ensure that your choice of a front-end framework doesn't call the method every time, but only once to load the data or when specifically needed (for example when the data gets updated due to user action or when the user requests it). + +### Publication replacements + +Using methods has its limitations and there are other tools that you might want to evaluate as a potential replacement: + +- [Grapher](https://github.com/cult-of-coders/grapher) is a favorite answer and allows you to easily blend with GraphQL +- [Apollo GraphQL](https://www.apollographql.com/) has an [integration package](https://atmospherejs.com/meteor/apollo) with Meteor +- REST APIs for simpler use cases + +Do note, that you can mix all of these based on your needs. + +### Low observer reuse + +Observers are among the key components of Meteor. They take care of observing documents on MongoDB and they notify changes. Creating them is an expensive operation, so you want to make sure that Meteor reuses them as much as possible. + +> [Learn more about observers](https://docs.montiapm.com/academy/know-your-observers) + +The key for observer reuse is to make sure that the queries requested are identical. This means that user given values should be standardized and so should any dynamic input like time. Publications for users should check if user is signed in first before returning publication and if user is not signed in, then it should instead call `this.ready();`. + +```js +Meteor.publish('userPosts', function() { + // Good: Check auth first to enable observer reuse + if (!this.userId) { + return this.ready(); + } + + return Posts.find({ userId: this.userId }); +}); +``` + +> [Learn more on improving observer reuse](https://docs.montiapm.com/academy/improving-cpu-network-usage) + +### Redis Oplog + +[Redis Oplog](https://atmospherejs.com/cultofcoders/redis-oplog) is a popular solution to Meteor's Oplog tailing (which ensures the reactivity, but has some severe limitations that especially impact performance). Redis Oplog as name suggests uses [Redis](https://redis.io/) to track changes to data that you only need and cache them. This reduces load on the server and database, allows you to track only the data that you want and only publish the changes you need. + +## Methods + +While methods are listed as one of the possible replacements for publications, they themselves can be made more performant. After all it really depends on what you put inside them and APM will provide you with the necessary insight on which methods are the problem. + +### Heavy actions + +In general heavy tasks that take a lot of resources or take long and block the server for that time should be taken out and instead be run in its own server that focuses just on running those heavy tasks. This can be another Meteor server or even better something specifically optimized for that given task. + +### Reoccurring jobs + +Reoccurring jobs are another prime candidate to be taken out into its own application. What this means is that you will have an independent server that is going to be tasked with running the reoccurring jobs and the main application will only add to the list and be recipient of the results, most likely via database results. + +### Rate limiting + +Rate limit your methods to reduce effectiveness of DDoS attacks and spare your server. This is also a good practice to ensure that you don't accidentally DDoS yourself. For example a user who clicks multiple times on a button that triggers an expensive function. + +In this example you should also in general ensure that any button that triggers a server event should be disabled until there is a response from the server that the event has finished. + +You can and should rate limit both methods and subscriptions. + +> [Learn more about rate limiting](/api/DDPRateLimiter) + +## MongoDB + +The following section offers some guidance on optimizing performance of your Meteor application when it comes to the database. You can find these and more information in other places that deal with MongoDB performance optimization, like on the [official MongoDB website](https://www.mongodb.com/basics/best-practices). These are all applicable, and you should spend some time researching into them as well. The guide here offers some initial and most common patterns. + +### IP whitelisting + +If your MongoDB hosting provider allows it, you should make sure that you whitelist the IPs of your application servers. If you don't then your database servers are likely to come under attack from hackers trying to brute force their way in. Besides the security risk this also impacts performance as authentication is not a cheap operation and it will impact performance. + +See the [Galaxy container environment guide](https://help.galaxycloud.app/en/article/container-environment-lfd6kh/) on IP whitelisting to get IPs for your Galaxy servers. + +### Indexes + +While single indexes on one field are helpful on simple query calls, you will most likely have more advanced queries with multiple variables. To cover those you will need to create compound indexes. For example: + +```js +await Statistics.createIndexAsync( + { + pageId: 1, + language: 1, + date: 1 + }, + { unique: true } +); +``` + +When creating indexes you should sort the variables in ESR (Equality, Sort, Range) style: + +1. First you put variables that will be equal to something specific +2. Second you put variables that sort things +3. Third variables that provide range for that query + +Further you should order these variables in a way that the fields that filter the most should be first. + +Make sure that all the indexes are used and remove unused indexes as leaving unused indexes will have negative impact on performance as the database will have to still keep track on all the indexed variables. + +### Find strategies + +To optimize finds ensure that all queries are indexed. Meaning that any `.find()` variables should be indexed as described above. + +All your finds should have a limit on the return so that the database stops going through the data once it has reached the limit, and you only return the limited number of results instead of the whole database. + +Beware of queries with `n + 1` issue. For example in a database that has cars and car owners. You don't want to get cars, and then call the database for each car owner, instead you want to use only two queries. One where you get all the cars and second where you get all the owners and then match the data on the front-end. + +```js +// Bad: N+1 queries +const cars = await Cars.find().fetchAsync(); +for (const car of cars) { + const owner = await Owners.findOneAsync({ _id: car.ownerId }); // N queries! +} + +// Good: 2 queries +const cars = await Cars.find().fetchAsync(); +const ownerIds = cars.map(car => car.ownerId); +const owners = await Owners.find({ _id: { $in: ownerIds } }).fetchAsync(); +``` + +Additional tips: + +- Check all queries that run longer than 100ms as there might be issues +- Do not use RegEx for your queries as these queries have to go through all the data to do that match +- If you still have issues make sure that you read data from secondaries + +### Beware of collection hooks + +While collection hooks can help in many cases beware of them and make sure that you understand how they work as they might create additional queries that you might not know about. Make sure to review packages that use them so that they won't create additional queries. + +### Caching + +Once your user base increases you want to invest into query caching like using Redis, Redis Oplog and other. For more complex queries or when you are retrieving data from multiple collections, then you want to use [aggregation](https://www.mongodb.com/docs/manual/aggregation/) and save their results. + +## Scaling + +### Vertical and horizontal scaling + +There are mainly two different ways of scaling: the vertical and horizontal one. + +- **Vertical scaling** boils down to adding more resources (CPU/RAM/disk) to your containers +- **Horizontal scaling** refers to adding more machines or containers to your pool of resources + +Horizontal scaling for Meteor projects typically includes running multiple instances of your app on a single container with multiple cores, or running multiple instances on multiple containers. + +### Container autoscaling + +It is important to be ready for sudden spikes of traffic. While all the other measures mentioned here will help, at a certain point it becomes impossible to support more users on one container and additional containers need to be added to support these users. + +Today most hosting solutions offer scaling triggers that you can set to automatically scale up (and down) the number of containers for your app based on things like number of connections, CPU and RAM usage. Galaxy has these as well. Learn more about [setting triggers for scaling on Galaxy](https://help.galaxycloud.app/en/article/scaling-meteor-apps-1wxc9dq/). + +Setting this is vital, so that your application can keep on running when you have extra people come and then saves you money by scaling down when the containers are not in use. + +When initially setting these pay a close attention to the performance of your app. You need to learn when is the right time to scale your app so it has enough time to spin up new containers before the existing ones get overwhelmed by traffic. + +There are other points to pay attention to as well. For example if your app is used by a corporation you might want to setup that on weekdays the minimum number of containers is going to increase just before the start of working hours and then decrease the minimum to 1 for after hours and on weekends. + +Usually when you are working on performance issues you will have higher numbers of containers as you optimize your app. It is therefore vital to revisit your scaling settings after each round of improvements to ensure that scaling triggers are properly optimized. + +## Packages + +During development, it is very tempting to add packages to solve issues or support some features. This should be done carefully and each package should be vetted carefully if it is a good fit for the application. + +Besides security and maintenance issues you also want to know which dependencies a given package introduces and as a whole what will be the impact on performance. + +## Further reading + +- [Monti APM Documentation](https://docs.montiapm.com/) +- [Monti APM Academy](https://docs.montiapm.com/academy/know-your-observers) - Learn about observers, CPU optimization, and more +- [MongoDB Best Practices](https://www.mongodb.com/basics/best-practices) +- [Redis Oplog](https://atmospherejs.com/cultofcoders/redis-oplog) +- [WebSocket Compression](/performance/websocket-compression) diff --git a/v3-docs/docs/troubleshooting/hot-code-push.md b/v3-docs/docs/troubleshooting/hot-code-push.md new file mode 100644 index 0000000000..599544f692 --- /dev/null +++ b/v3-docs/docs/troubleshooting/hot-code-push.md @@ -0,0 +1,218 @@ +# Hot Code Push Troubleshooting + +Is your Meteor Cordova app not getting the updates you're deploying? + +After reading this article, you'll know: + +1. The prerequisites to using Hot Code Push +2. Techniques to diagnose and solve common issues +3. How to dig deeper if that doesn't solve your issue + +This article builds on the [Cordova article](/about/cordova). We recommend reading that first, though we've tried to link back to its relevant sections. + +## Prerequisites + +Make sure that you have: + +- An Android and/or iOS mobile app based on Meteor's Cordova integration +- The package `hot-code-push` listed in your `.meteor/versions` file +- **Locally:** Make sure your test device and development device are on the same network +- **In production:** Make sure the `--server` flag of your `meteor build` command points to the same place as your `ROOT_URL` environment variable (or, on Galaxy, the *site* in `meteor deploy site`) + +## Known issues + +### Override compatibility versions + +Did the app suddenly stop getting new code after you updated Meteor, or you changed plugins? + +The client probably logs: `Skipping downloading new version because the Cordova platform version or plugin versions have changed and are potentially incompatible` + +Meteor, Cordova and plugins cannot be updated through Hot Code Push. So Meteor by default disables Hot Code Push to app versions that have different versions than the server. This avoids crashing a user's app, for example, when new JS calls a plugin that their app version doesn't yet have. + +You can override this behavior by setting the `AUTOUPDATE_VERSION` environment variable. Just make sure you deal with potentially incompatible versions in your JS instead. + +### Update your AUTOUPDATE_VERSION + +`AUTOUPDATE_VERSION` is an environment variable you can add to your `run` and `deploy` commands: + +```bash +AUTOUPDATE_VERSION=abc meteor deploy example.com +``` + +If your app has an `AUTOUPDATE_VERSION` set, make sure you change its value when you want a deploy to update your clients. + +### Cordova doesn't hot reload CSS separately + +Are you seeing your web app incorporate changes without reload, yet your Cordova app reloads each time? + +For CSS-only changes, this is the expected behavior. Browsers update the layout without reload, but in Cordova, any change reloads the whole app. + +### Outdated custom reload code and packages + +There are several reload packages on Atmosphere, and maybe your app includes some custom reload code. These may have bugs or be outdated. + +In particular, when you push an update, does the app reload but use the old code anyway? Probably, the code hasn't been updated to work with newer Meteor versions. We recommend you call `WebAppLocalServer.switchToPendingVersion` before forcing a browser reload. + +Alternatively, use the built-in behavior to reload. Instead of, say, `window.location.reload()`, call the `retry` function passed to the `Reload._onMigrate()` callback: + +```js +Reload._onMigrate((retry) => { + if (/* not ready */) { + window.setTimeout(retry, 5 * 1000); // Check again in 5 seconds + return [false]; + } + // ready + return [true]; +}); +``` + +If you use a package that is no longer compatible, consider forking it or opening a PR with the above changes. Alternatively, you can switch to a compatible one such as [`quave:reloader`](https://github.com/quavedev/reloader). + +### Avoid hash fragments + +Cordova doesn't show the URL bar, but the user is still on some URL or other, which may have a hash (`#`). HCP works better if it doesn't have one. + +If you can, remove the hash fragment before the reload. + +### Avoid making it download big files + +In the client-side logs, you may see HCP fail with errors like: + +``` +Error: Error downloading asset: / + at http://localhost:12472/plugins/cordova-plugin-meteor-webapp/www/webapp-local-server.js:51:21 + at Object.callbackFromNative (http://localhost:12472/cordova.js:287:58) + at :1:9 +``` + +This error from `cordova-plugin-meteor-webapp` may be caused by big files, often in the `public` folder. Downloading these can fail depending on connection speed, and available space on the device. + +You could run `du -a public | sort -n -r | head -n 20` to find the 20 biggest files and their sizes. Consider serving them from an external storage service or CDN instead. Then they are only downloaded when really needed, and can fail downloading without blocking HCP. + +### If it is only broken locally + +If you notice HCP works in production but not when you test locally, you may need to enable clear text or set a correct `--mobile-server`. See the [autoupdate package documentation](/packages/autoupdate) for details. + +## Still having issues? + +If none of that solved your issues and you'd like to dive deeper, here are some tips to get you started. + +If you end up finding a bug in one of Meteor's packages or plugins, don't hesitate to open an [issue](https://github.com/meteor/meteor/issues) and/or a [pull request](https://github.com/meteor/meteor/pulls). + +### Where does hot code push live? + +Hot code push is included in `meteor-base` through a web of official Meteor packages, most importantly `reload` and `autoupdate`. + +In the case of Cordova, a lot of the heavy lifting is done by `cordova-plugin-meteor-webapp`. + +To oversimplify, `autoupdate` decides *when* to refresh the client, the plugin then downloads the new client code and assets, and `reload` then refreshes the page to start using them. + +### What are the steps it takes? + +We can break it down a bit more: + +1. Whenever the server thinks the client side may have changed, it calculates a hash of your entire client bundle +2. It publishes this hash to all clients +3. The clients subscribe to this publish +4. When a new hash arrives, each client compares it to its own hash +5. If it's different, it starts to download the new client bundle +6. When it's done, the client saves any data and announces that it will reload +7. The app and packages get a chance to save their data or to deny the reload +8. If/when allowed, it reloads + +### How to spy on it? + +To figure out where the issue is, we can log the various steps HCP takes. + +First, make sure you can see client-side logs using your browser's developer tools or remote debugging for mobile devices. + +A few more useful values to print, and events to listen to, might be: + +**The version hashes:** + +```js +console.log(__meteor_runtime_config__.autoupdate.versions['web.cordova']); +``` + +**The reactive `Autoupdate.newClientAvailable()`:** + +If this turns into `true` and then doesn't refresh, you know the client does receive the new version but something goes wrong trying to download or apply it. + +```js +Tracker.autorun(() => { + console.log('new client available:', Autoupdate.newClientAvailable()); +}); +``` + +**Check if we finish downloading and preparing the new version:** + +```js +WebAppLocalServer.onNewVersionReady(() => { + console.log('new version is ready!'); + // Copied from original in autoupdate/autoupdate_cordova.js because we overwrite it + if (Package.reload) { + Package.reload.Reload._reload(); + } +}); +``` + +**Check if permission to reload is being requested:** + +Be sure to return `[true]` or the reload may not happen. + +```js +Reload._onMigrate(() => { + console.log('going to reload now'); + return [true]; +}); +``` + +**Know if a run of `Meteor.startup` was the result of a HCP reload or not:** + +We can take advantage of the fact that `Session`s (like `ReactiveDict`s) are preserved. + +```js +Meteor.startup(() => { + console.log('Was HCP:', Session.get('wasHCP')); + Session.set('wasHCP', false); + + Reload._onMigrate(() => { + Session.set('wasHCP', true); + return [true]; + }); +}); +``` + +## How to edit the source + +Finally, if you want to change some of the package and plugin code locally, you can. + +### Editing the packages + +Say we want to edit the `autoupdate` package. + +In the root of your project, create a folder named `packages`, then add a folder `autoupdate`. Here we put the code from the original package (found in `~/.meteor/packages`), then we edit it. + +Meteor will now use the local version instead of the official one. + +### Editing the plugin + +To install a modified version of a plugin: + +1. From another folder, download the original code: + ```bash + git clone https://github.com/meteor/cordova-plugin-meteor-webapp.git + ``` + +2. Install it into your Meteor project: + ```bash + meteor add cordova:cordova-plugin-meteor-webapp@file://path/to/cordova-plugin-meteor-webapp + ``` + +3. Modify it as you like + +Meteor will start using the local version instead of the official one. But note you will have to rerun `meteor build` or `meteor run` every time you change the plugin. + +## Found a bug? + +If you found a bug in one of the packages or plugins, don't hesitate to open an [issue](https://github.com/meteor/meteor/issues) and/or [pull request](https://github.com/meteor/meteor/pulls). diff --git a/v3-docs/docs/tutorials/accounts/accounts.md b/v3-docs/docs/tutorials/accounts/accounts.md new file mode 100644 index 0000000000..8d5d3154c3 --- /dev/null +++ b/v3-docs/docs/tutorials/accounts/accounts.md @@ -0,0 +1,603 @@ +# Users and Accounts + +After reading this article, you'll know: + +1. What features in core Meteor enable user accounts +2. How to use accounts-ui for a quick prototype +3. How to build a fully-featured password login experience +4. How to enable login through OAuth providers like Facebook +5. How to add custom data to Meteor's users collection +6. How to manage user roles and permissions + +## Features in core Meteor + +Before we get into all of the different user-facing accounts functionality you can add with Meteor, let's go over some of the features built into the Meteor DDP protocol and `accounts-base` package. These are the parts of Meteor that you'll definitely need to be aware of if you have any user accounts in your app; most of everything else is optional and added/removed via packages. + +### userId in DDP + +DDP is Meteor's built-in pub/sub and RPC protocol. You can read about how to use it in the [Data Loading](/tutorials/data-loading/data-loading) and [Methods](/tutorials/methods/methods) articles. In addition to the concepts of data loading and method calls, DDP has one more feature built in - the idea of a `userId` field on a connection. This is the place where login state is tracked, regardless of which accounts UI package or login service you are using. + +This built-in feature means that you always get `this.userId` inside Methods and Publications, and can access the user ID on the client. This is a great starting point for building your own custom accounts system, but most developers won't need to worry about the mechanics, since you'll mostly be interacting with the `accounts-base` package instead. + +### accounts-base + +This package is the core of Meteor's developer-facing user accounts functionality. This includes: + +1. A users collection with a standard schema, accessed through [`Meteor.users`](/api/accounts#Meteor-users), and the client-side singletons [`Meteor.userId()`](/api/accounts#Meteor-userId) and [`Meteor.user()`](/api/accounts#Meteor-user), which represent the login state on the client. +2. A variety of helpful other generic methods to keep track of login state, log out, validate users, etc. Visit the [Accounts section of the docs](/api/accounts) to find a complete list. +3. An API for registering new login handlers, which is used by all of the other accounts packages to integrate with the accounts system. + +Usually, you don't need to include `accounts-base` yourself since it's added for you if you use `accounts-password` or similar, but it's good to be aware of what is what. + +## Fast prototyping with accounts-ui + +Often, a complicated accounts system is not the first thing you want to build when you're starting out with a new app, so it's useful to have something you can drop in quickly. This is where `accounts-ui` comes in - it's one line that you drop into your app to get an accounts system. To add it: + +```bash +meteor add accounts-ui +``` + +Then include it anywhere in a Blaze template: + +```html +{{> loginButtons}} +``` + +Then, make sure to pick a login provider; they will automatically integrate with `accounts-ui`: + +```bash +# pick one or more of the below +meteor add accounts-password +meteor add accounts-facebook +meteor add accounts-google +meteor add accounts-github +meteor add accounts-twitter +meteor add accounts-meetup +meteor add accounts-meteor-developer +``` + +Now open your app, follow the configuration steps, and you're good to go - if you've done one of our [Meteor tutorials](/tutorials/react/1.creating-the-app), you've already seen this in action. Of course, in a production application, you probably want a more custom user interface and some logic to have a more tailored UX, but that's why we have the rest of these tutorials. + +## Password login + +Meteor comes with a secure and fully-featured password login system out of the box. To use it, add the package: + +```bash +meteor add accounts-password +``` + +To see what options are available to you, read the complete description of the [`accounts-password` API in the Meteor docs](/api/accounts). + +### Requiring username or email + +By default, the `Accounts.createUser` function provided by `accounts-password` allows you to create an account with a username, email, or both. Most apps expect a specific combination of the two, so you will certainly want to validate the new user creation: + +```js +// Ensuring every user has an email address, should be in server-side code +Accounts.validateNewUser((user) => { + new SimpleSchema({ + _id: { type: String }, + emails: { type: Array }, + 'emails.$': { type: Object }, + 'emails.$.address': { type: String }, + 'emails.$.verified': { type: Boolean }, + createdAt: { type: Date }, + services: { type: Object, blackbox: true } + }).validate(user); + + // Return true to allow user creation to proceed + return true; +}); +``` + +### Multiple emails + +Often, users might want to associate multiple email addresses with the same account. `accounts-password` addresses this case by storing the email addresses as an array in the user collection. There are some handy API methods to deal with [adding](/api/accounts#Accounts-addEmail), [removing](/api/accounts#Accounts-removeEmail), and [verifying](/api/accounts#Accounts-verifyEmail) emails. + +One useful thing to add for your app can be the concept of a "primary" email address. This way, if the user has added multiple emails, you know where to send confirmation emails and similar. + +### Case sensitivity + +Meteor handles case sensitivity for email addresses and usernames. Since MongoDB doesn't have a concept of case-insensitive indexes, it was impossible to guarantee unique emails at the database level. For this reason, we have some special APIs for querying and updating users which manage the case-sensitivity problem at the application level. + +**What does this mean for your app?** + +Follow one rule: don't query the database by `username` or `email` directly. Instead, use the [`Accounts.findUserByUsername`](/api/accounts#Accounts-findUserByUsername) and [`Accounts.findUserByEmail`](/api/accounts#Accounts-findUserByEmail) methods provided by Meteor. This will run a query for you that is case-insensitive, so you will always find the user you are looking for. + +### Email flows + +When you have a login system for your app based on user emails, that opens up the possibility for email-based account flows. The common thing between all of these workflows is that they involve sending a unique link to the user's email address, which does something special when it is clicked. Let's look at some common examples that Meteor's `accounts-password` package supports out of the box: + +1. **Password reset.** When the user clicks the link in their email, they are taken to a page where they can enter a new password for their account. +2. **User enrollment.** A new user is created by an administrator, but no password is set. When the user clicks the link in their email, they are taken to a page where they can set a new password for their account. +3. **Email verification.** When the user clicks the link in their email, the application records that this email does indeed belong to the correct user. + +#### Sending the email + +`accounts-password` comes with handy functions that you can call from the server to send an email: + +1. [`Accounts.sendResetPasswordEmail`](/api/accounts#Accounts-sendResetPasswordEmail) +2. [`Accounts.sendEnrollmentEmail`](/api/accounts#Accounts-sendEnrollmentEmail) +3. [`Accounts.sendVerificationEmail`](/api/accounts#Accounts-sendVerificationEmail) + +The email is generated using the email templates from [`Accounts.emailTemplates`](/api/accounts#Accounts-emailTemplates), and include links generated with `Accounts.urls`. + +#### Identifying when the link is clicked + +When the user receives the email and clicks the link inside, their web browser will take them to your app. Now, you need to be able to identify these special links and act appropriately. If you haven't customized the link URL, then you can use some built-in callbacks to identify when the app is in the middle of an email flow: + +1. [`Accounts.onResetPasswordLink`](/api/accounts#Accounts-onResetPasswordLink) +2. [`Accounts.onEnrollmentLink`](/api/accounts#Accounts-onEnrollmentLink) +3. [`Accounts.onEmailVerificationLink`](/api/accounts#Accounts-onEmailVerificationLink) + +Here's how you would use one of these functions: + +```js +Accounts.onResetPasswordLink(async (token, done) => { + // Display the password reset UI, get the new password... + + try { + await Accounts.resetPasswordAsync(token, newPassword); + // Resume normal operation + done(); + } catch (err) { + // Display error + console.error('Password reset failed:', err); + } +}); +``` + +If you want a different URL for your reset password page, you need to customize it using the `Accounts.urls` option: + +```js +Accounts.urls.resetPassword = (token) => { + return Meteor.absoluteUrl(`reset-password/${token}`); +}; +``` + +If you have customized the URL, you will need to add a new route to your router that handles the URL you have specified. + +#### Completing the process + +When the user submits the form, you need to call the appropriate function to commit their change to the database: + +1. [`Accounts.resetPasswordAsync`](/api/accounts#Accounts-resetPassword) - this one should be used both for resetting the password, and enrolling a new user; it accepts both kinds of tokens. +2. [`Accounts.verifyEmailAsync`](/api/accounts#Accounts-verifyEmail) + +After you have called one of the two functions above or the user has cancelled the process, call the `done` function you got in the link callback. + +### Customizing accounts emails + +You will probably want to customize the emails `accounts-password` will send on your behalf. This can be done through the [`Accounts.emailTemplates` API](/api/accounts#Accounts-emailTemplates). Below is some example code: + +```js +Accounts.emailTemplates.siteName = "Meteor Guide Todos Example"; +Accounts.emailTemplates.from = "Meteor Todos Accounts "; + +Accounts.emailTemplates.resetPassword = { + subject(user) { + return "Reset your password on Meteor Todos"; + }, + text(user, url) { + return `Hello! +Click the link below to reset your password on Meteor Todos. +${url} +If you didn't request this email, please ignore it. +Thanks, +The Meteor Todos team +`; + }, + html(user, url) { + // This is where HTML email content would go. + // See the section about html emails below. + } +}; +``` + +#### HTML emails + +If you've ever needed to deal with sending pretty HTML emails from an app, you know that it can quickly become a nightmare. Compatibility of popular email clients with basic HTML features like CSS is notoriously spotty. Start with a [responsive email template](https://github.com/leemunroe/responsive-html-email-template) or [framework](https://get.foundation/emails), and then use a tool to convert your email content into something that is compatible with all email clients. + +## OAuth login + +Meteor supports popular login providers through OAuth out of the box. + +### Facebook, Google, and more + +Here's a complete list of login providers for which Meteor actively maintains core packages: + +1. Facebook with `accounts-facebook` +2. Google with `accounts-google` +3. GitHub with `accounts-github` +4. Twitter with `accounts-twitter` +5. Meetup with `accounts-meetup` +6. Meteor Developer Accounts with `accounts-meteor-developer` + +### Logging in + +If you are using an off-the-shelf login UI like `accounts-ui`, you don't need to write any code after adding the relevant package. If you are building a login experience from scratch, you can log in programmatically using the [`Meteor.loginWith`](/api/accounts#Meteor-loginWithExternalService) function: + +```js +try { + await Meteor.loginWithFacebookAsync({ + requestPermissions: ['user_friends', 'public_profile', 'email'] + }); + // successful login! +} catch (err) { + // handle error + console.error('Login failed:', err); +} +``` + +### Configuring OAuth + +There are a few points to know about configuring OAuth login: + +1. **Client ID and secret.** It's best to keep your OAuth secret keys outside of your source code, and pass them in through Meteor.settings. Read how in the [Security article](/tutorials/security/security#api-keys). +2. **Redirect URL.** On the OAuth provider's side, you'll need to specify a *redirect URL*. The URL will look like: `https://www.example.com/_oauth/facebook`. Replace `facebook` with the name of the service you are using. Note that you will need to configure two URLs - one for your production app, and one for your development environment, where the URL might be something like `http://localhost:3000/_oauth/facebook`. +3. **Permissions.** Each login service provider should have documentation about which permissions are available. If you want additional permissions to the user's data when they log in, pass some of these strings in the `requestPermissions` option. + +### Calling service API for more data + +If your app supports or even requires login with an external service such as Facebook, it's natural to also want to use that service's API to request additional data about that user. + +First, you'll need to request the relevant permissions when logging in the user. + +Then, you need to get the user's access token. You can find this token in the `Meteor.users` collection under the `services` field: + +```js +// Given a userId, get the user's Facebook access token +const user = await Meteor.users.findOneAsync(userId); +const fbAccessToken = user.services.facebook.accessToken; +``` + +Now that you have the access token, you can use the `fetch` API or an npm package to access the service's API directly. + +## Loading and displaying user data + +Meteor's accounts system, as implemented in `accounts-base`, also includes a database collection and generic functions for getting data about users. + +### Currently logged in user + +Once a user is logged into your app with one of the methods described above, it is useful to be able to identify which user is logged in, and get the data provided during the registration process. + +#### On the client: Meteor.userId() + +For code that runs on the client, the global `Meteor.userId()` reactive function will give you the ID of the currently logged in user. + +In addition to that core API, there are some helpful shorthand helpers: `Meteor.user()`, which is exactly equal to calling `Meteor.users.findOne(Meteor.userId())`, and the `{{currentUser}}` Blaze helper that returns the value of `Meteor.user()`. + +#### On the server: this.userId + +On the server, each connection has a different logged in user, so there is no global logged-in user state by definition. We suggest using the `this.userId` property on the context of Methods and publications, and passing that around through function arguments to wherever you need it. + +```js +// Accessing this.userId inside a publication +Meteor.publish('lists.private', function() { + if (!this.userId) { + return this.ready(); + } + + return Lists.find({ + userId: this.userId + }, { + fields: Lists.publicFields + }); +}); +``` + +```js +// Accessing this.userId inside a Method +Meteor.methods({ + async 'todos.updateText'({ todoId, newText }) { + new SimpleSchema({ + todoId: { type: String }, + newText: { type: String } + }).validate({ todoId, newText }); + + const todo = await Todos.findOneAsync(todoId); + + if (!todo.editableBy(this.userId)) { + throw new Meteor.Error('todos.updateText.unauthorized', + 'Cannot edit todos in a private list that is not yours'); + } + + await Todos.updateAsync(todoId, { + $set: { text: newText } + }); + } +}); +``` + +### The Meteor.users collection + +Meteor comes with a default MongoDB collection for user data. It's stored in the database under the name `users`, and is accessible in your code through `Meteor.users`. The schema of a user document in this collection will depend on which login service was used to create the account. + +Here's an example of a user that created their account with `accounts-password`: + +```js +{ + "_id": "DQnDpEag2kPevSdJY", + "createdAt": "2015-12-10T22:34:17.610Z", + "services": { + "password": { + "bcrypt": "XXX" + }, + "resume": { + "loginTokens": [ + { + "when": "2015-12-10T22:34:17.615Z", + "hashedToken": "XXX" + } + ] + } + }, + "emails": [ + { + "address": "ada@lovelace.com", + "verified": false + } + ] +} +``` + +Here's what the same user would look like if they instead logged in with Facebook: + +```js +{ + "_id": "Ap85ac4r6Xe3paeAh", + "createdAt": "2015-12-10T22:29:46.854Z", + "services": { + "facebook": { + "accessToken": "XXX", + "expiresAt": 1454970581716, + "id": "XXX", + "email": "ada@lovelace.com", + "name": "Ada Lovelace", + "first_name": "Ada", + "last_name": "Lovelace" + }, + "resume": { + "loginTokens": [...] + } + } +} +``` + +Note that the schema is different when users register with different login services. There are a few things to be aware of when dealing with this collection: + +1. User documents in the database have secret data like access keys and hashed passwords. When [publishing user data to the client](#publishing-custom-data), be extra careful not to include anything that client shouldn't be able to see. +2. DDP, Meteor's data publication protocol, only knows how to resolve conflicts in top-level fields. This means that you can't have one publication send `services.facebook.first_name` and another send `services.facebook.locale` - one of them will win. The best way to fix this is to denormalize the data you want onto custom top-level fields. +3. When finding users by email or username, make sure to use the case-insensitive functions provided by `accounts-password`. + +## Custom data about users + +As your app gets more complex, you will invariably need to store some data about individual users, and the most natural place to put that data is in additional fields on the `Meteor.users` collection. + +### Add top-level fields onto the user document + +The best way to store your custom data onto the `Meteor.users` collection is to add a new uniquely-named top-level field on the user document: + +```js +// Using address schema from schema.org +const newMailingAddress = { + addressCountry: 'US', + addressLocality: 'Seattle', + addressRegion: 'WA', + postalCode: '98052', + streetAddress: "20341 Whitworth Institute 405 N. Whitworth" +}; + +await Meteor.users.updateAsync(userId, { + $set: { + mailingAddress: newMailingAddress + } +}); +``` + +You can use any field name other than those [used by the Accounts system](/api/accounts#Meteor-users). + +### Adding fields on user registration + +Sometimes, you want to set a field when the user first creates their account. You can do this using [`Accounts.onCreateUser`](/api/accounts#Accounts-onCreateUser): + +```js +// Generate user initials after Facebook login +Accounts.onCreateUser((options, user) => { + if (!user.services.facebook) { + throw new Error('Expected login with Facebook only.'); + } + + const { first_name, last_name } = user.services.facebook; + user.initials = first_name[0].toUpperCase() + last_name[0].toUpperCase(); + + // We still want the default hook's 'profile' behavior. + if (options.profile) { + user.profile = options.profile; + } + + // Don't forget to return the new user object at the end! + return user; +}); +``` + +Note that the `user` object provided doesn't have an `_id` field yet. If you need to do something with the new user's ID inside this function, you can generate the ID yourself: + +```js +import { Random } from 'meteor/random'; + +// Generate a todo list for each new user +Accounts.onCreateUser(async (options, user) => { + // Generate a user ID ourselves + user._id = Random.id(); + + // Use the user ID we generated + await Lists.createListForUser(user._id); + + // Don't forget to return the new user object at the end! + return user; +}); +``` + +### Don't use profile + +There's a tempting existing field called `profile` that is added by default when a new user registers. This field was historically intended to be used as a scratch pad for user-specific data. Because of this, **the `profile` field on every user is automatically writeable by that user from the client**. It's also automatically published to the client for that particular user. + +It turns out that having a field writeable by default without making that super obvious might not be the best idea. There are many stories of new Meteor developers storing fields such as `isAdmin` on `profile`... and then a malicious user can set that to true whenever they want, making themselves an admin. + +Rather than dealing with the specifics of this field, it can be helpful to ignore its existence entirely. You can safely do that as long as you deny all writes from the client: + +```js +// Deny all client-side updates to user documents +Meteor.users.deny({ + update() { return true; } +}); +``` + +### Publishing custom data + +If you want to access the custom data you've added to the `Meteor.users` collection in your UI, you'll need to publish it to the client. The most important thing to keep in mind is that user documents contain private data about your users—hashed passwords and access keys for external APIs. This means it's critically important to filter the fields of the user document that you send to any client. + +```js +Meteor.publish('Meteor.users.initials', function ({ userIds }) { + // Validate the arguments to be what we expect + new SimpleSchema({ + userIds: { type: Array }, + 'userIds.$': { type: String } + }).validate({ userIds }); + + // Select only the users that match the array of IDs passed in + const selector = { + _id: { $in: userIds } + }; + + // Only return one field, `initials` + const options = { + fields: { initials: 1 } + }; + + return Meteor.users.find(selector, options); +}); +``` + +### Preventing unnecessary data retrieval + +Take care storing lots of custom data on the user document, particularly data which grows indefinitely, because by default the entire user document is fetched from the database whenever a user tries to log in or out. + +A new `options` parameter was added to some methods which retrieves a user document. This parameter can include a [mongo field specifier](/api/collections#fieldspecifiers) to include or omit specific fields from the query: + +```js +// fetch only the user's name from the database: +const user = await Meteor.userAsync({ fields: { "profile.name": 1 } }); +const name = user?.profile?.name; + +// check if an email exists without fetching their entire document: +const userExists = !!await Accounts.findUserByEmail(email, { fields: { _id: 1 } }); + +// get the user id from a userName: +const user = await Accounts.findUserByUsername(userName, { fields: { _id: 1 } }); +const userId = user?._id; +``` + +You can also use [`Accounts.config({defaultFieldSelector: {...}})`](/api/accounts#AccountsCommon-config) to include or omit specific user fields by default: + +```js +Accounts.config({ + defaultFieldSelector: { + username: 1, + emails: 1, + createdAt: 1, + profile: 1, + services: 1, + } +}); +``` + +Or omit fields with large amounts of data: + +```js +Accounts.config({ defaultFieldSelector: { myBigArray: 0 } }); +``` + +## Roles and permissions + +One of the main reasons you might want to add a login system to your app is to have permissions for your data. For example, if you were running a forum, you would want administrators or moderators to be able to delete any post, but normal users can only delete their own. This uncovers two different types of permissions: + +1. Role-based permissions +2. Per-document permissions + +### alanning:roles + +The most popular package for role-based permissions in Meteor is [`alanning:roles`](https://atmospherejs.com/alanning/roles). For example, here is how you would make a user into an administrator, or a moderator: + +```js +// Give Alice the 'admin' role +await Roles.addUsersToRolesAsync(aliceUserId, 'admin', Roles.GLOBAL_GROUP); + +// Give Bob the 'moderator' role for a particular category +await Roles.addUsersToRolesAsync(bobsUserId, 'moderator', categoryId); +``` + +Now, let's say you wanted to check if someone was allowed to delete a particular forum post: + +```js +const forumPost = await Posts.findOneAsync(postId); + +const canDelete = await Roles.userIsInRoleAsync( + userId, + ['admin', 'moderator'], + forumPost.categoryId +); + +if (!canDelete) { + throw new Meteor.Error('unauthorized', + 'Only admins and moderators can delete posts.'); +} + +await Posts.removeAsync(postId); +``` + +Note that we can check for multiple roles at once, and if someone has a role in the `GLOBAL_GROUP`, they are considered as having that role in every group. + +Read more in the [`alanning:roles` package documentation](https://atmospherejs.com/alanning/roles). + +### Per-document permissions + +Sometimes, it doesn't make sense to abstract permissions into "groups" - you want documents to have owners and that's it. In this case, you can use a simpler strategy using collection helpers: + +```js +Lists.helpers({ + editableBy(userId) { + if (!this.userId) { + return false; + } + return this.userId === userId; + } +}); +``` + +Now, we can call this simple function to determine if a particular user is allowed to edit this list: + +```js +const list = await Lists.findOneAsync(listId); + +if (!list.editableBy(userId)) { + throw new Meteor.Error('unauthorized', + 'Only list owners can edit private lists.'); +} +``` + +Learn more about how to use collection helpers in the [Collections article](/tutorials/collections/collections#collection-helpers). + +## Best practices summary + +1. **Use accounts-password** for email/password login and add OAuth packages as needed. +2. **Validate new users** with `Accounts.validateNewUser` to ensure required fields. +3. **Use case-insensitive queries** with `Accounts.findUserByEmail` and `Accounts.findUserByUsername`. +4. **Customize email templates** using `Accounts.emailTemplates` for professional communications. +5. **Never use the profile field** for sensitive data—deny client-side writes to user documents. +6. **Add custom data to top-level fields** on user documents, not nested in profile. +7. **Always filter fields** when publishing user data to clients. +8. **Use alanning:roles** for role-based access control. +9. **Use collection helpers** for per-document permissions. +10. **Configure defaultFieldSelector** to optimize user document fetching. diff --git a/v3-docs/docs/tutorials/apollo/apollo.md b/v3-docs/docs/tutorials/apollo/apollo.md new file mode 100644 index 0000000000..3274276cb5 --- /dev/null +++ b/v3-docs/docs/tutorials/apollo/apollo.md @@ -0,0 +1,546 @@ +# Apollo and GraphQL + +This guide covers integrating Apollo and GraphQL with Meteor applications. + +After reading this guide, you'll know: + +1. What Apollo and GraphQL are and why you might use them +2. How to set up Apollo Server with Meteor +3. How to set up Apollo Client for querying data +4. How to integrate Meteor accounts with Apollo +5. Best practices for using GraphQL in Meteor apps + +## Introduction + +Apollo is a GraphQL client/server for transporting data. While Meteor's built-in pub/sub system is excellent for real-time reactivity with MongoDB, Apollo provides a way to get data from any database or API using GraphQL's powerful query language. + +**When to use Apollo/GraphQL:** + +- You need to fetch data from multiple sources (not just MongoDB) +- You want clients to specify exactly what data they need +- You're building a larger application with complex data requirements +- You need to integrate with external GraphQL APIs + +**When to stick with Meteor's pub/sub:** + +- You need real-time reactivity out of the box +- You're primarily using MongoDB +- You want the simplest possible data layer +- You need optimistic UI with automatic rollback + +## Quick start + +You can create a new Meteor application with Apollo pre-configured: + +```bash +meteor create apollo-app --apollo +``` + +This sets up both Apollo Server and Client with Meteor integration. + +## GraphQL basics + +GraphQL is a query language for APIs. Instead of the server deciding what's in a publication, the client uses GraphQL to specify exactly which fields of which objects it wants. + +### Schema definition + +Define your data types and operations: + +```graphql +# imports/apollo/schema.graphql +type User { + _id: ID! + username: String + email: String + profile: Profile +} + +type Profile { + name: String + avatar: String +} + +type Query { + me: User + user(id: ID!): User + users(limit: Int): [User] +} + +type Mutation { + updateProfile(name: String, avatar: String): User +} +``` + +### Resolvers + +Resolvers are functions that fetch the data for each field: + +```js +// server/resolvers.js +export const resolvers = { + Query: { + me: async (obj, args, { user }) => { + if (!user) return null; + return user; + }, + + user: async (obj, { id }) => { + return await Meteor.users.findOneAsync(id); + }, + + users: async (obj, { limit = 10 }) => { + return await Meteor.users.find({}, { limit }).fetchAsync(); + } + }, + + Mutation: { + updateProfile: async (obj, { name, avatar }, { user }) => { + if (!user) { + throw new Error('Not authenticated'); + } + + await Meteor.users.updateAsync(user._id, { + $set: { + 'profile.name': name, + 'profile.avatar': avatar + } + }); + + return await Meteor.users.findOneAsync(user._id); + } + } +}; +``` + +## Apollo Server setup + +### Installation + +Install the required packages: + +```bash +meteor add apollo +meteor npm install @apollo/server express body-parser graphql +``` + +### Server configuration + +Set up Apollo Server with Meteor: + +```js +// server/apollo.js +import { ApolloServer } from '@apollo/server'; +import { expressMiddleware } from '@apollo/server/express4'; +import { WebApp } from 'meteor/webapp'; +import { getUser } from 'meteor/apollo'; +import express from 'express'; +import { json } from 'body-parser'; +import typeDefs from '/imports/apollo/schema.graphql'; +import { resolvers } from '/server/resolvers'; + +const context = async ({ req }) => ({ + user: await getUser(req.headers.authorization) +}); + +const server = new ApolloServer({ + typeDefs, + resolvers, + cache: 'bounded', +}); + +export async function startApolloServer() { + await server.start(); + + WebApp.connectHandlers.use( + '/graphql', + express() + .disable('etag') + .disable('x-powered-by') + .use(json()) + .use(expressMiddleware(server, { context })) + ); + + console.log('Apollo Server ready at /graphql'); +} +``` + +Start the server in your main server file: + +```js +// server/main.js +import { startApolloServer } from './apollo'; + +Meteor.startup(async () => { + await startApolloServer(); +}); +``` + +### Loading GraphQL files + +To import `.graphql` files, you may need to configure your build. Alternatively, you can define schemas as strings: + +```js +// imports/apollo/schema.js +import { gql } from 'graphql-tag'; + +export const typeDefs = gql` + type User { + _id: ID! + username: String + email: String + } + + type Query { + me: User + users: [User] + } +`; +``` + +## Apollo Client setup + +### Installation + +```bash +meteor npm install @apollo/client graphql +``` + +### Client configuration + +```js +// client/apollo.js +import { ApolloClient, InMemoryCache, HttpLink, ApolloLink } from '@apollo/client'; +import { Accounts } from 'meteor/accounts-base'; + +const httpLink = new HttpLink({ + uri: '/graphql', +}); + +// Add authentication header +const authLink = new ApolloLink((operation, forward) => { + const token = Accounts._storedLoginToken(); + + operation.setContext({ + headers: { + authorization: token ? `Bearer ${token}` : '', + }, + }); + + return forward(operation); +}); + +export const client = new ApolloClient({ + link: authLink.concat(httpLink), + cache: new InMemoryCache(), +}); +``` + +### React integration + +Wrap your app with ApolloProvider: + +```jsx +// client/main.jsx +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { ApolloProvider } from '@apollo/client'; +import { Meteor } from 'meteor/meteor'; +import { client } from './apollo'; +import App from '/imports/ui/App'; + +Meteor.startup(() => { + const container = document.getElementById('react-target'); + const root = createRoot(container); + + root.render( + + + + ); +}); +``` + +## Querying data + +### Basic queries + +Use the `useQuery` hook to fetch data: + +```jsx +import React from 'react'; +import { useQuery, gql } from '@apollo/client'; + +const GET_USERS = gql` + query GetUsers($limit: Int) { + users(limit: $limit) { + _id + username + email + } + } +`; + +function UserList() { + const { loading, error, data } = useQuery(GET_USERS, { + variables: { limit: 10 } + }); + + if (loading) return

Loading...

; + if (error) return

Error: {error.message}

; + + return ( +
    + {data.users.map(user => ( +
  • {user.username}
  • + ))} +
+ ); +} +``` + +### Refetching and polling + +```jsx +function UserList() { + const { loading, error, data, refetch } = useQuery(GET_USERS, { + // Poll every 30 seconds for updates + pollInterval: 30000, + }); + + return ( + <> + + {/* ... */} + + ); +} +``` + +## Mutations + +### Basic mutations + +Use the `useMutation` hook to modify data: + +```jsx +import React, { useState } from 'react'; +import { useMutation, gql } from '@apollo/client'; + +const UPDATE_PROFILE = gql` + mutation UpdateProfile($name: String, $avatar: String) { + updateProfile(name: $name, avatar: $avatar) { + _id + profile { + name + avatar + } + } + } +`; + +function ProfileEditor() { + const [name, setName] = useState(''); + const [updateProfile, { loading, error }] = useMutation(UPDATE_PROFILE); + + const handleSubmit = async (e) => { + e.preventDefault(); + + try { + await updateProfile({ + variables: { name } + }); + // Success! + } catch (err) { + console.error('Update failed:', err); + } + }; + + return ( +
+ setName(e.target.value)} + placeholder="Your name" + /> + + {error &&

Error: {error.message}

} +
+ ); +} +``` + +### Updating the cache + +After a mutation, you can update the local cache: + +```jsx +const [createUser] = useMutation(CREATE_USER, { + update(cache, { data: { createUser } }) { + cache.modify({ + fields: { + users(existingUsers = []) { + const newUserRef = cache.writeFragment({ + data: createUser, + fragment: gql` + fragment NewUser on User { + _id + username + } + ` + }); + return [...existingUsers, newUserRef]; + } + } + }); + } +}); +``` + +## Meteor accounts integration + +The `meteor/apollo` package provides user integration: + +### Server context + +```js +import { getUser } from 'meteor/apollo'; + +const context = async ({ req }) => ({ + user: await getUser(req.headers.authorization) +}); +``` + +### Using user in resolvers + +```js +const resolvers = { + Query: { + myData: async (obj, args, { user }) => { + if (!user) { + throw new Error('Authentication required'); + } + + return await MyCollection.find({ userId: user._id }).fetchAsync(); + } + }, + + Mutation: { + createItem: async (obj, { title }, { user }) => { + if (!user) { + throw new Error('Authentication required'); + } + + const itemId = await Items.insertAsync({ + title, + userId: user._id, + createdAt: new Date() + }); + + return await Items.findOneAsync(itemId); + } + } +}; +``` + +## Subscriptions (Real-time data) + +While Apollo supports GraphQL subscriptions, for real-time data in Meteor you might consider: + +1. **Polling** - Simple and works well for many use cases +2. **Meteor pub/sub** - Use alongside Apollo for real-time needs +3. **WebSocket subscriptions** - Full GraphQL subscription support + +### Setting up subscriptions + +```bash +meteor npm install graphql-ws ws +``` + +```js +// server/apollo.js +import { WebSocketServer } from 'ws'; +import { useServer } from 'graphql-ws/lib/use/ws'; +import { makeExecutableSchema } from '@graphql-tools/schema'; + +const schema = makeExecutableSchema({ typeDefs, resolvers }); + +// Create WebSocket server +const wsServer = new WebSocketServer({ + server: WebApp.httpServer, + path: '/graphql-ws', +}); + +useServer({ schema }, wsServer); +``` + +## Community packages + +Several community packages provide additional features: + +- **[quave:graphql](https://atmospherejs.com/quave/graphql)** - Utility package for standard GraphQL setup +- **[cultofcoders:apollo](https://atmospherejs.com/cultofcoders/apollo)** - Comprehensive Meteor & Apollo integration +- **[cultofcoders:graphql-loader](https://atmospherejs.com/cultofcoders/graphql-loader)** - Easy GraphQL schema loading +- **[cultofcoders:apollo-accounts](https://atmospherejs.com/cultofcoders/apollo-accounts)** - Meteor accounts in GraphQL +- **[swydo:blaze-apollo](https://atmospherejs.com/swydo/blaze-apollo)** - Blaze integration for Apollo Client + +## Comparing Apollo with Meteor pub/sub + +| Feature | Meteor Pub/Sub | Apollo/GraphQL | +|---------|---------------|----------------| +| Real-time updates | Built-in | Requires subscriptions | +| Data sources | MongoDB | Any source | +| Client specifies fields | No | Yes | +| Optimistic UI | Built-in | Manual cache updates | +| Learning curve | Lower | Higher | +| Type safety | Optional | Schema-enforced | +| Latency | Streaming | All-at-once | + +## Best practices + +1. **Use both when appropriate** - Meteor pub/sub for real-time MongoDB data, Apollo for complex queries or external APIs. + +2. **Define schemas clearly** - Good GraphQL schemas document your API automatically. + +3. **Handle errors gracefully** - Use Apollo's error handling in resolvers: + ```js + import { GraphQLError } from 'graphql'; + + throw new GraphQLError('Not authorized', { + extensions: { code: 'FORBIDDEN' } + }); + ``` + +4. **Use DataLoader for batching** - Prevent N+1 query problems: + ```js + import DataLoader from 'dataloader'; + + const userLoader = new DataLoader(async (ids) => { + const users = await Meteor.users.find({ _id: { $in: ids } }).fetchAsync(); + return ids.map(id => users.find(user => user._id === id)); + }); + ``` + +5. **Keep resolvers thin** - Move business logic to separate modules. + +6. **Use fragments** - Reuse field selections across queries: + ```graphql + fragment UserFields on User { + _id + username + email + } + + query GetUsers { + users { + ...UserFields + } + } + ``` + +## Further reading + +- [Apollo Documentation](https://www.apollographql.com/docs/) +- [GraphQL Official Site](https://graphql.org/) +- [Principled GraphQL](https://principledgraphql.com/) diff --git a/v3-docs/docs/tutorials/application-structure/index.md b/v3-docs/docs/tutorials/application-structure/index.md index c48ae722d9..3993c31c8c 100644 --- a/v3-docs/docs/tutorials/application-structure/index.md +++ b/v3-docs/docs/tutorials/application-structure/index.md @@ -10,7 +10,7 @@ After reading this article, you'll know: ## Universal JavaScript -Meteor is a *full-stack* framework for building JavaScript applications. This means Meteor applications differ from most applications in that they include code that runs on the client, inside a web browser or Cordova mobile app, code that runs on the server, inside a [Node.js](http://nodejs.org/) container, and _common_ code that runs in both environments. The [Meteor build tool](https://guide.meteor.com/build-tool) allows you to specify what JavaScript code, including any supporting UI templates, CSS rules, and static assets, to run in each environment using a combination of ES2015 `import` and `export` and the Meteor build system [default file load order](#default-file-load-order) rules. +Meteor is a *full-stack* framework for building JavaScript applications. This means Meteor applications differ from most applications in that they include code that runs on the client, inside a web browser or Cordova mobile app, code that runs on the server, inside a [Node.js](http://nodejs.org/) container, and _common_ code that runs in both environments. The [Meteor build tool](/about/build-tool) allows you to specify what JavaScript code, including any supporting UI templates, CSS rules, and static assets, to run in each environment using a combination of ES2015 `import` and `export` and the Meteor build system [default file load order](#default-file-load-order) rules. ### ES2015 modules @@ -31,7 +31,7 @@ import './loading.html'; // import Blaze compiled HTML from relativ import '/imports/ui/style.css'; // import CSS from absolute path ``` -> For more ways to import styles, see the [Build System](https://guide.meteor.com/build-tool#css-importing) article. +> For more ways to import styles, see the [Build System](/about/build-tool#css-importing) article. Meteor also supports the standard ES2015 modules `export` syntax: @@ -71,7 +71,7 @@ If you need to `require` from an ES2015 module with a `default` export, you can ### Using CoffeeScript -See the Docs: [CoffeeScript](https://docs.meteor.com/packages/coffeescript.html#coffeescript) +See the Docs: [CoffeeScript](/packages/coffeescript#coffeescript) ```cs // lists.coffee @@ -265,13 +265,13 @@ By default, any JavaScript files in your Meteor application folder are bundled a - **tests** - Any directory named `tests/` is not loaded anywhere. Use this for any test code you want to run using a test runner outside of [Meteor's built-in test tools](https://guide.meteor.com/testing.html). + Any directory named `tests/` is not loaded anywhere. Use this for any test code you want to run using a test runner outside of [Meteor's built-in test tools](/tutorials/testing/testing). The following directories are also not loaded as part of your app code: - Files/directories whose names start with a dot, like `.meteor` and `.git` - `packages/`: Used for local packages -- `cordova-build-override/`: Used for [advanced mobile build customizations](https://guide.meteor.com/mobile.html#advanced-build) +- `cordova-build-override/`: Used for [advanced mobile build customizations](/about/cordova#advanced-build) - `programs`: For legacy reasons ### Files outside special directories @@ -298,7 +298,7 @@ However there are some challenges to splitting your code in this way that should ### Sharing code -The primary challenge is properly sharing code between the different applications you are building. The simplest approach to deal with this issue is to deploy the *same* application on different web servers, controlling the behavior via different [settings](https://guide.meteor.com/deployment.html#environment). This approach allows you to deploy different versions with different scaling behavior but doesn't enjoy most of the other advantages stated above. +The primary challenge is properly sharing code between the different applications you are building. The simplest approach to deal with this issue is to deploy the *same* application on different web servers, controlling the behavior via different [settings](/tutorials/deployment/deployment#environment). This approach allows you to deploy different versions with different scaling behavior but doesn't enjoy most of the other advantages stated above. If you want to create Meteor applications with separate code, you'll have some modules that you'd like to share between them. If those modules are something the wider world could use, you should consider [publishing them to a package system](../../packages/#writing-atmosphere-packages), either npm or Atmosphere, depending on whether the code is Meteor-specific or otherwise. diff --git a/v3-docs/docs/tutorials/blaze/2.collections.md b/v3-docs/docs/tutorials/blaze/2.collections.md index c605f2fdf4..197d455822 100644 --- a/v3-docs/docs/tutorials/blaze/2.collections.md +++ b/v3-docs/docs/tutorials/blaze/2.collections.md @@ -2,7 +2,7 @@ Meteor already sets up MongoDB for you. In order to use our database, we need to create a _collection_, which is where we will store our _documents_, in our case our `tasks`. -> You can read more about collections [here](https://v3-docs.meteor.com/api/collections.html). +> You can read more about collections [here](/api/collections). In this step we will implement all the necessary code to have a basic collection for our tasks up and running. @@ -21,7 +21,7 @@ export const TasksCollection = new Mongo.Collection("tasks"); Notice that we stored the file in the `imports/api` directory, which is a place to store API-related code, like publications and methods. You can name this folder as you want, this is just a choice. -> You can read more about app structure and imports/exports [here](http://guide.meteor.com/structure.html). +> You can read more about app structure and imports/exports [here](/tutorials/application-structure/). ### 2.2: Initialize Tasks Collection {#initialize-tasks-collection} @@ -85,7 +85,7 @@ But wait! Something is missing. If you run your app now, you'll see that you don That's because we need to publish our data to the client. -> For more information on Publications/Subscriptions, please check our [docs](https://v3-docs.meteor.com/api/meteor.html#pubsub). +> For more information on Publications/Subscriptions, please check our [docs](/api/meteor#pubsub). Meteor doesn't need REST calls. It instead relies on synchronizing the MongoDB on the server with a MiniMongoDB on the client. It does this by first publishing collections on the server and then subscribing to them on the client. diff --git a/v3-docs/docs/tutorials/blaze/3.forms-and-events.md b/v3-docs/docs/tutorials/blaze/3.forms-and-events.md index 053aafe0c3..83c69b40b0 100644 --- a/v3-docs/docs/tutorials/blaze/3.forms-and-events.md +++ b/v3-docs/docs/tutorials/blaze/3.forms-and-events.md @@ -72,7 +72,7 @@ You also can style it as you wish. For now, we only need some margin at the top Now let's create a function to handle the form submit and insert a new task into the database. To do it, we will need to implement a Meteor Method. -Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](https://guide.meteor.com/methods.html). +Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](/tutorials/methods/methods). To create your methods, you can create a file called `TasksMethods.js`. @@ -179,7 +179,7 @@ Finally, in the last line of the event handler, we need to clear the input to pr ### 3.5: Show Newest Tasks First -Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](https://guide.meteor.com/collections.html#mongo-collections) query. +Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](/tutorials/collections/collections#mongo-collections) query. ::: code-group diff --git a/v3-docs/docs/tutorials/blaze/6.filter-tasks.md b/v3-docs/docs/tutorials/blaze/6.filter-tasks.md index 623193cd03..a6235f4a12 100644 --- a/v3-docs/docs/tutorials/blaze/6.filter-tasks.md +++ b/v3-docs/docs/tutorials/blaze/6.filter-tasks.md @@ -6,7 +6,7 @@ In this step, you will filter your tasks by status and show the number of pendin First, you will add a button to show or hide the completed tasks from the list. -To keep the state, we will use the `ReactiveDict`, a reactive dictionary which enables us to store an arbitrary set of key-value pairs. Use it to manage the internal state in your components, i.e. the currently selected item in a list. To know more about how `ReactiveDict` works, you can click on this [link](https://docs.meteor.com/api/reactive-dict.html), and there you will find everything you need to know and everything you can do with it. +To keep the state, we will use the `ReactiveDict`, a reactive dictionary which enables us to store an arbitrary set of key-value pairs. Use it to manage the internal state in your components, i.e. the currently selected item in a list. To know more about how `ReactiveDict` works, you can click on this [link](/api/ReactiveDict), and there you will find everything you need to know and everything you can do with it. We need to install the `reactive-dict` package in our app. Simply run the command below on your app root directory: @@ -88,7 +88,7 @@ The button in the UI to toggle our state will look something like this: ``` ::: -You may notice we’re using `if` (a conditional test) for the first time, and it’s pretty straightforward. You can learn more about the conditional test, `if`, [here](https://guide.meteor.com/v1.3/blaze.html#builtin-block-helpers). We’re also using a helper called `hideCompleted` that we didn’t create yet, but we will shortly. +You may notice we’re using `if` (a conditional test) for the first time, and it’s pretty straightforward. You can learn more about the conditional test, `if`, [here](http://blazejs.org/api/spacebars.html#If-Unless). We’re also using a helper called `hideCompleted` that we didn’t create yet, but we will shortly. ### 6.2: Button style diff --git a/v3-docs/docs/tutorials/blaze/7.adding-user-accounts.md b/v3-docs/docs/tutorials/blaze/7.adding-user-accounts.md index e5c2ef125a..225d51e8c6 100644 --- a/v3-docs/docs/tutorials/blaze/7.adding-user-accounts.md +++ b/v3-docs/docs/tutorials/blaze/7.adding-user-accounts.md @@ -8,7 +8,7 @@ Meteor already comes with a basic authentication and account management system o meteor add accounts-password ``` -> There are many more authentication methods supported. You can read more about the accounts system [here](https://v3-docs.meteor.com/api/accounts.html). +> There are many more authentication methods supported. You can read more about the accounts system [here](/api/accounts). We also recommend you to install `bcrypt` node module, otherwise, you are going to see a warning saying that you are using a pure-Javascript implementation of it. diff --git a/v3-docs/docs/tutorials/blaze/8.deploying.md b/v3-docs/docs/tutorials/blaze/8.deploying.md index 49d27a882e..de4b39c88f 100644 --- a/v3-docs/docs/tutorials/blaze/8.deploying.md +++ b/v3-docs/docs/tutorials/blaze/8.deploying.md @@ -1,6 +1,6 @@ ## 8: Deploying -Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](https://guide.meteor.com/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. +Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](/tutorials/deployment/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. In this tutorial, we will deploy our app on [Galaxy](https://galaxycloud.app/), which is our own cloud solution. Galaxy offers a free plan, so you can deploy and test your app. Pretty cool, right? diff --git a/v3-docs/docs/tutorials/code-style/code-style.md b/v3-docs/docs/tutorials/code-style/code-style.md new file mode 100644 index 0000000000..510b8d67d9 --- /dev/null +++ b/v3-docs/docs/tutorials/code-style/code-style.md @@ -0,0 +1,491 @@ +# Code Style + +After reading this article, you'll know: + +1. Why it's a good idea to have consistent code style +2. Which style guide we recommend for JavaScript code +3. How to set up ESLint to check code style automatically +4. Style suggestions for Meteor-specific patterns, such as Methods, publications, and more + +## Benefits of consistent style + +Countless hours have been spent by developers throughout the years arguing over single vs. double quotes, where to put brackets, how many spaces to type, and all kinds of other cosmetic code style questions. These are all questions that have at best a tangential relationship to code quality, but are very easy to have opinions about because they are so visual. + +While it's not necessarily important whether your code base uses single or double quotes for string literals, there are huge benefits to making that decision once and having it be consistent across your organization. + +### Easy to read code + +The same way that you don't read English sentences one word at a time, you don't read code one token at a time. Mostly you just look at the shape of a certain expression, or the way it highlights in your editor, and assume what it does. If the style of every bit of code is consistent, that ensures that bits of code that look the same actually *are* the same—there isn't any hidden punctuation or gotchas that you don't expect, so you can focus on understanding the logic instead of the symbols. + +```js +// This code is misleading because it looks like both statements +// are inside the conditional. +if (condition) + firstStatement(); + secondStatement(); +``` + +```js +// Much clearer! +if (condition) { + firstStatement(); +} + +secondStatement(); +``` + +### Automatic error checking + +Having a consistent style means that it's easier to adopt standard tools for error checking. For example, if you adopt a convention that you must always use `let` or `const` instead of `var`, you can now use a tool to ensure all of your variables are scoped the way you expect. That means you can avoid bugs where variables act in unexpected ways. Also, by enforcing that all variables are declared before use, you can catch typos before even running any code! + +### Deeper understanding + +It's hard to learn everything about a programming language at once. For example, programmers new to JavaScript often struggle with the `var` keyword and function scope. Using a community-recommended coding style with automatic linting can warn you about these pitfalls proactively. This means you can jump right into coding without learning about all of the edge cases of JavaScript ahead of time. + +## JavaScript style guide + +Here at Meteor, we strongly believe that JavaScript is the best language to build web applications. JavaScript is constantly improving, and modern ES2015+ features have really brought together the JavaScript community. + +### Use modern JavaScript + +Meteor's `ecmascript` package compiles modern JavaScript down to regular JavaScript that all browsers can understand using the [Babel compiler](https://babeljs.io/). It's fully backwards compatible to "regular" JavaScript, so you don't have to use any new features if you don't want to. + +The `ecmascript` package is included in all new apps and packages by default, and compiles all files with the `.js` file extension automatically. + +Key modern JavaScript features to use: + +- **Arrow functions**: `(x) => x * 2` +- **Async/await**: `async function getData() { await fetchData(); }` +- **Destructuring**: `const { name, email } = user;` +- **Template literals**: `` `Hello, ${name}!` `` +- **Modules**: `import { method } from './module';` +- **Classes**: `class MyClass extends BaseClass { }` +- **Spread operator**: `const newArray = [...oldArray, newItem];` + +### Follow a JavaScript style guide + +We recommend choosing and sticking to a JavaScript style guide and enforcing it with tools. A popular option that we recommend is the [Airbnb style guide](https://github.com/airbnb/javascript) with the ES6 extensions (and optionally React extensions). + +## Check your code with ESLint + +"Code linting" is the process of automatically checking your code for common errors or style problems. For example, ESLint can determine if you have made a typo in a variable name, or some part of your code is unreachable because of a poorly written `if` condition. + +We recommend using the [Airbnb eslint configuration](https://github.com/airbnb/javascript/tree/master/packages/eslint-config-airbnb) which verifies the Airbnb styleguide. + +### Installing and running ESLint + +To setup ESLint in your application, install the following npm packages: + +```bash +meteor npm install --save-dev eslint @meteorjs/eslint-config-meteor eslint-plugin-meteor +``` + +For React projects, add React-specific plugins: + +```bash +meteor npm install --save-dev eslint-plugin-react eslint-plugin-react-hooks eslint-plugin-jsx-a11y +``` + +::: tip +Meteor comes with npm bundled so that you can type `meteor npm` without worrying about installing it yourself. +::: + +### Configuration + +Create an `.eslintrc.json` file in your project root or add an `eslintConfig` section to your `package.json`: + +```json +{ + "extends": "@meteorjs/eslint-config-meteor", + "env": { + "browser": true, + "node": true + }, + "rules": { + "no-console": "warn" + } +} +``` + +Add lint scripts to your `package.json`: + +```json +{ + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint . --fix", + "pretest": "npm run lint --silent" + } +} +``` + +To run the linter: + +```bash +meteor npm run lint +``` + +### Modern ESLint flat config + +ESLint 9+ uses a new flat config format. Here's an example `eslint.config.js`: + +```js +import js from '@eslint/js'; +import meteorPlugin from 'eslint-plugin-meteor'; + +export default [ + js.configs.recommended, + { + plugins: { + meteor: meteorPlugin, + }, + languageOptions: { + ecmaVersion: 2022, + sourceType: 'module', + globals: { + Meteor: 'readonly', + Package: 'readonly', + Npm: 'readonly', + }, + }, + rules: { + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'prefer-const': 'error', + 'no-var': 'error', + }, + }, +]; +``` + +### Integrating with your editor + +Linting is the fastest way to find potential bugs in your code. Running a linter is usually faster than running your app or your unit tests, so it's a good idea to run it all the time. + +#### Visual Studio Code + +1. Install the [ESLint extension](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) +2. Open Command Palette (`Ctrl+Shift+P` / `Cmd+Shift+P`) +3. Search for "ESLint: Enable ESLint" + +Add to your VS Code settings for automatic fixing on save: + +```json +{ + "editor.codeActionsOnSave": { + "source.fixAll.eslint": true + } +} +``` + +#### WebStorm + +WebStorm has built-in ESLint support: + +1. Go to Settings > Languages & Frameworks > JavaScript > Code Quality Tools > ESLint +2. Select "Automatic ESLint configuration" +3. Enable "Run eslint --fix on save" + +#### Sublime Text + +Install these packages through Package Control: + +- SublimeLinter +- SublimeLinter-eslint + +## Meteor code style + +The section above talked about JavaScript code in general. However, there are some style questions that are Meteor-specific, in particular how to name and structure all of the different components of your app. + +### Collections + +Collections should be named as a plural noun, in PascalCase. The name of the collection in the database (the first argument to the collection constructor) should be the same as the name of the JavaScript symbol. + +```js +// Defining a collection +export const Lists = new Mongo.Collection('lists'); +``` + +Fields in the database should be camelCased just like your JavaScript variable names: + +```js +// Inserting a document with camelCased field names +await Widgets.insertAsync({ + myFieldName: 'Hello, world!', + otherFieldName: 'Goodbye.', + createdAt: new Date() +}); +``` + +### Methods and publications + +Method and publication names should be camelCased, and namespaced to the module they are in: + +```js +// in imports/api/todos/methods.js +import { createMethod } from 'meteor/jam:method'; + +export const updateText = createMethod({ + name: 'todos.updateText', + schema: new SimpleSchema({ + todoId: { type: String }, + newText: { type: String } + }), + async run({ todoId, newText }) { + // ... + } +}); +``` + +For classic Meteor methods: + +```js +Meteor.methods({ + async 'todos.updateText'({ todoId, newText }) { + // ... + } +}); +``` + +Here's how this naming convention looks when applied to a publication: + +```js +// Naming a publication +Meteor.publish('lists.public', function listsPublic() { + return Lists.find({ userId: { $exists: false } }); +}); +``` + +### Async patterns + +In Meteor 3, always use async/await patterns: + +```js +// Good - async patterns +Meteor.methods({ + async 'items.create'(data) { + const id = await Items.insertAsync(data); + return id; + }, + + async 'items.update'(id, changes) { + await Items.updateAsync(id, { $set: changes }); + } +}); +``` + +```js +// Avoid - sync patterns (deprecated in Meteor 3) +Meteor.methods({ + 'items.create'(data) { + const id = Items.insert(data); // Deprecated + return id; + } +}); +``` + +### Files, exports, and packages + +You should use the ES2015 `import` and `export` features to manage your code. This will let you better understand the dependencies between different parts of your code, and it will help you navigate to the source code of a dependency. + +Each file in your app should represent one logical module. Avoid having catch-all utility modules that export a variety of unrelated functions and symbols. + +When a file represents a single class or UI component, the file should be named the same as the thing it defines, with the same capitalization: + +```js +// ClickCounter.js +export default class ClickCounter { + // ... +} +``` + +When you import it: + +```js +import ClickCounter from './ClickCounter.js'; +``` + +For Atmosphere packages, you'll use named exports: + +```js +// You'll need to destructure here +import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; +``` + +### React components + +Name React components using PascalCase, with one component per file: + +```jsx +// TodoItem.jsx +import React from 'react'; + +export function TodoItem({ todo, onToggle }) { + return ( +
  • onToggle(todo._id)}> + + {todo.text} + +
  • + ); +} +``` + +Use hooks for state and side effects: + +```jsx +// TodoList.jsx +import React from 'react'; +import { useSubscribe, useFind } from 'meteor/react-meteor-data'; +import { Todos } from '/imports/api/todos/todos'; +import { TodoItem } from './TodoItem'; + +export function TodoList({ listId }) { + const isLoading = useSubscribe('todos.inList', listId); + const todos = useFind(() => Todos.find({ listId }), [listId]); + + if (isLoading()) { + return
    Loading...
    ; + } + + return ( +
      + {todos.map(todo => ( + + ))} +
    + ); +} +``` + +### Blaze templates + +Since Spacebars templates are global and need to have names that are completely unique across the whole app, we recommend naming your Blaze templates with the full path to the namespace, separated by underscores: + +```html + +``` + +If this template is a "smart" component that loads server data and accesses the router, append `_page` to the name: + +```html + +``` + +Often when you are dealing with templates or UI components, you'll have several closely coupled files to manage: + +``` +imports/ui/lists/ +├── show.html +├── show.js +└── show.css +``` + +### Directory structure + +Organize your code by feature rather than by type: + +``` +imports/ +├── api/ +│ ├── todos/ +│ │ ├── todos.js # Collection definition +│ │ ├── methods.js # Methods +│ │ ├── publications.js # Publications +│ │ └── schema.js # Schema definition +│ └── lists/ +│ ├── lists.js +│ ├── methods.js +│ └── publications.js +├── ui/ +│ ├── components/ # Reusable components +│ │ ├── Button.jsx +│ │ └── Loading.jsx +│ ├── pages/ # Page components +│ │ ├── HomePage.jsx +│ │ └── TodoPage.jsx +│ └── layouts/ +│ └── MainLayout.jsx +└── startup/ + ├── client/ + │ └── index.js + └── server/ + └── index.js +``` + +## TypeScript considerations + +If using TypeScript, follow these additional conventions: + +```typescript +// Use interfaces for object types +interface Todo { + _id: string; + text: string; + checked: boolean; + listId: string; + createdAt: Date; +} + +// Use type for unions and intersections +type TodoStatus = 'pending' | 'completed' | 'archived'; + +// Export types alongside values +export interface CreateTodoParams { + text: string; + listId: string; +} + +export async function createTodo(params: CreateTodoParams): Promise { + return await Todos.insertAsync({ + ...params, + checked: false, + createdAt: new Date() + }); +} +``` + +## Prettier integration + +For automatic code formatting, combine ESLint with Prettier: + +```bash +meteor npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier +``` + +Update your `.eslintrc.json`: + +```json +{ + "extends": [ + "@meteorjs/eslint-config-meteor", + "plugin:prettier/recommended" + ] +} +``` + +Create a `.prettierrc` file: + +```json +{ + "semi": true, + "singleQuote": true, + "trailingComma": "es5", + "printWidth": 100, + "tabWidth": 2 +} +``` + +## Summary + +1. **Use modern JavaScript** - Take advantage of ES2015+ features like async/await, destructuring, and modules. +2. **Follow a style guide** - The Airbnb style guide is a popular choice. +3. **Use ESLint** - Catch errors and enforce style automatically. +4. **Integrate with your editor** - Get immediate feedback as you code. +5. **Be consistent** - Whatever conventions you choose, apply them consistently. +6. **Use async/await** - Meteor 3 uses async patterns throughout. +7. **Organize by feature** - Group related files together. diff --git a/v3-docs/docs/tutorials/collections/collections.md b/v3-docs/docs/tutorials/collections/collections.md new file mode 100644 index 0000000000..7ab5b58ee7 --- /dev/null +++ b/v3-docs/docs/tutorials/collections/collections.md @@ -0,0 +1,574 @@ +# Collections and Schemas + +After reading this guide, you'll know: + +1. The different types of MongoDB collections in Meteor, and how to use them. +2. How to define a schema for a collection to control its content. +3. What to consider when defining your collection's schema. +4. How to enforce the schema when writing to a collection. +5. How to carefully change the schema of your collection. +6. How to deal with associations between records. + +## MongoDB collections in Meteor + +At its core, a web application offers its users a view into, and a way to modify, a persistent set of data. Whether managing a list of todos, or ordering a car to pick you up, you are interacting with a permanent but constantly changing data layer. + +In Meteor, that data layer is typically stored in MongoDB. A set of related data in MongoDB is referred to as a "collection". In Meteor you access MongoDB through [collections](/api/collections#Mongo-Collection), making them the primary persistence mechanism for your app data. + +However, collections are a lot more than a way to save and retrieve data. They also provide the core of the interactive, connected user experience that users expect from the best applications. Meteor makes this user experience easy to implement. + +In this article, we'll look closely at how collections work in various places in the framework, and how to get the most out of them. + +### Server-side collections + +When you create a collection on the server: + +```js +const Todos = new Mongo.Collection('todos'); +``` + +You are creating a collection within MongoDB, and an interface to that collection to be used on the server. In Meteor 3, all database operations are asynchronous: + +```js +// This line returns a promise that resolves when the insert is done +await Todos.insertAsync({ _id: 'my-todo', text: 'Learn Meteor 3' }); + +// So we need to await when querying too +const todo = await Todos.findOneAsync({ _id: 'my-todo' }); + +console.log(todo); +``` + +### Client-side collections + +On the client, when you write the same line: + +```js +const Todos = new Mongo.Collection('todos'); +``` + +It does something totally different! + +On the client, there is no direct connection to the MongoDB database. Instead, on the client, a collection is a client-side *cache* of the database. This is achieved thanks to the [Minimongo](https://github.com/meteor/meteor/blob/master/packages/minimongo/README.md) library—an in-memory, all JS, implementation of the MongoDB API. + +```js +// This line is changing an in-memory Minimongo data structure +await Todos.insertAsync({ _id: 'my-todo' }); + +// And this line is querying it +const todo = await Todos.findOneAsync({ _id: 'my-todo' }); + +// In Minimongo, this still happens very quickly! +console.log(todo); +``` + +The way that you move data from the server (and MongoDB-backed) collection into the client (in-memory) collection is the subject of the [Data Loading article](/tutorials/data-loading/data-loading). Generally speaking, you *subscribe* to a *publication*, which pushes data from the server to the client. Usually, you can assume that the client contains an up-to-date copy of some subset of the full MongoDB collection. + +To write data back to the server, you use a *Method*, the subject of the [Methods article](/tutorials/methods/methods). + +### Local collections + +There is a third way to use a collection in Meteor. On the client or server, if you create a collection in one of these two ways: + +```js +const SelectedTodos = new Mongo.Collection(null); +// or +const SelectedTodos = new Mongo.Collection('selectedtodos', { connection: null }); +``` + +This creates a *local collection*. This is a Minimongo collection that has no database connection (ordinarily a collection would either be directly connected to the database on the server, or via a subscription on the client). + +A local collection is a convenient way to use the full power of the Minimongo library for in-memory storage. For instance, you might use it instead of a simple array if you need to execute complex queries over your data. Or you may want to take advantage of its *reactivity* on the client to drive some UI in a way that feels natural in Meteor. + +## Defining a schema + +Although MongoDB is a schema-less database, which allows maximum flexibility in data structuring, it is generally good practice to use a schema to constrain the contents of your collection to conform to a known format. If you don't, then you tend to end up needing to write defensive code to check and confirm the structure of your data as it *comes out* of the database, instead of when it *goes into* the database. As in most things, you tend to *read data more often than you write it*, and so it's usually easier, and less buggy to use a schema when writing. + +In Meteor, the pre-eminent schema package is the npm [simpl-schema](https://www.npmjs.com/package/simpl-schema) package. It's an expressive, MongoDB based schema that's used to insert and update documents. Another alternative is [jagi:astronomy](https://atmospherejs.com/jagi/astronomy) which is a full Object Model (OM) layer offering schema definition, server/client side validators, object methods and event handlers. + +Let's assume that we have a `Lists` collection. To define a schema for this collection using `simpl-schema`, you can create a new instance of the `SimpleSchema` class and attach it to the `Lists` object: + +```js +import SimpleSchema from 'simpl-schema'; + +Lists.schema = new SimpleSchema({ + name: { type: String }, + incompleteCount: { type: Number, defaultValue: 0 }, + userId: { type: String, regEx: SimpleSchema.RegEx.Id, optional: true } +}); +``` + +This example from the Todos app defines a schema with a few simple rules: + +1. We specify that the `name` field of a list is required and must be a string. +2. We specify the `incompleteCount` is a number, which on insertion is set to `0` if not otherwise specified. +3. We specify that the `userId`, which is optional, must be a string that looks like the ID of a user document. + +We're using the SimpleSchema for Meteor related functionality, like IDs, but we encourage you to create custom regEx expressions for security reasons, for fields like `email` or `name`. Check out the [Simple Schema docs](https://github.com/longshotlabs/simpl-schema#regex) for more information. + +We attach the schema to the namespace of `Lists` directly, which allows us to check objects against this schema directly whenever we want, such as in a form or [Method](/tutorials/methods/methods). In the [next section](#schemas-on-write) we'll see how to use this schema automatically when writing to the collection. + +You can see that with relatively little code we've managed to restrict the format of a list significantly. You can read more about more complex things that can be done with schemas in the [Simple Schema docs](https://www.npmjs.com/package/simpl-schema). + +### Validating against a schema + +Now we have a schema, how do we use it? + +It's pretty straightforward to validate a document with a schema. We can write: + +```js +const list = { + name: 'My list', + incompleteCount: 3 +}; + +Lists.schema.validate(list); +``` + +In this case, as the list is valid according to the schema, the `validate()` line will run without problems. If however, we wrote: + +```js +const list = { + name: 'My list', + incompleteCount: 3, + madeUpField: 'this should not be here' +}; + +Lists.schema.validate(list); +``` + +Then the `validate()` call will throw a `ValidationError` which contains details about what is wrong with the `list` document. + +### The `ValidationError` + +What is a [`ValidationError`](https://github.com/meteor/validation-error/)? It's a special error that is used in Meteor to indicate a user-input based error in modifying a collection. Typically, the details on a `ValidationError` are used to mark up a form with information about what inputs don't match the schema. In the [Methods article](/tutorials/methods/methods#validation-error), we'll see more about how this works. + +## Designing your data schema + +Now that you are familiar with the basic API of Simple Schema, it's worth considering a few of the constraints of the Meteor data system that can influence the design of your data schema. Although generally speaking you can build a Meteor data schema much like any MongoDB data schema, there are some important details to keep in mind. + +The most important consideration is related to the way DDP, Meteor's data loading protocol, communicates documents over the wire. The key thing to realize is that DDP sends changes to documents at the level of top-level document *fields*. What this means is that if you have large and complex subfields on a document that change often, DDP can send unnecessary changes over the wire. + +For instance, in "pure" MongoDB you might design the schema so that each list document had a field called `todos` which was an array of todo items: + +```js +Lists.schema = new SimpleSchema({ + name: { type: String }, + todos: { type: Array }, + 'todos.$': { type: Object } +}); +``` + +The issue with this schema is that due to the DDP behavior just mentioned, each change to *any* todo item in a list will require sending the *entire* set of todos for that list over the network. This is because DDP has no concept of "change the `text` field of the 3rd item in the field called `todos`". It can only "change the field called `todos` to a totally new array". + +### Denormalization and multiple collections + +The implication of the above is that we need to create more collections to contain sub-documents. In the case of the Todos application, we need both a `Lists` collection and a `Todos` collection to contain each list's todo items. Consequently we need to do some things that you'd typically associate with a SQL database, like using foreign keys (`todo.listId`) to associate one document with another. + +In Meteor, it's often less of a problem doing this than it would be in a typical MongoDB application, as it's easy to publish overlapping sets of documents (we might need one set of users to render one screen of our app, and an intersecting set for another), which may stay on the client as we move around the application. So in that scenario there is an advantage to separating the subdocuments from the parent. + +However, given that MongoDB prior to version 3.2 doesn't support queries over multiple collections ("joins"), we typically end up having to denormalize some data back onto the parent collection. Denormalization is the practice of storing the same piece of information in the database multiple times (as opposed to a non-redundant "normal" form). MongoDB is a database where denormalizing is encouraged, and thus optimized for this practice. + +In the case of the Todos application, as we want to display the number of unfinished todos next to each list, we need to denormalize `list.incompleteTodoCount`. This is an inconvenience but typically reasonably easy to do as we'll see in the section on [abstracting denormalizers](#abstracting-denormalizers) below. + +Another denormalization that this architecture sometimes requires can be from the parent document onto sub-documents. For instance, in Todos, as we enforce privacy of the todo lists via the `list.userId` attribute, but we publish the todos separately, it might make sense to denormalize `todo.userId` also. To do so, we'd need to be careful to take the `userId` from the list when creating the todo, and updating all relevant todos whenever a list's `userId` changed. + +### Designing for the future + +An application, especially a web application, is rarely finished, and it's useful to consider potential future changes when designing your data schema. As in most things, it's rarely a good idea to add fields before you actually need them (often what you anticipate doesn't actually end up happening, after all). + +However, it's a good idea to think ahead to how the schema may change over time. For instance, you may have a list of strings on a document (perhaps a set of tags). Although it's tempting to leave them as a subfield on the document (assuming they don't change much), if there's a good chance that they'll end up becoming more complicated in the future (perhaps tags will have a creator, or subtags later on?), then it might be easier in the long run to make a separate collection from the beginning. + +The amount of foresight you bake into your schema design will depend on your app's individual constraints, and will need to be a judgement call on your part. + +### Schemas on write + +Although there are a variety of ways that you can run data through a Simple Schema before sending it to your collection (for instance you could check a schema in every method call), the simplest and most reliable is to use the [`aldeed:collection2`](https://atmospherejs.com/aldeed/collection2) package to run every mutator (`insert/update/upsert` call) through the schema. + +To do so, we use `attachSchema()`: + +```js +Lists.attachSchema(Lists.schema); +``` + +What this means is that now every time we call `Lists.insertAsync()`, `Lists.updateAsync()`, `Lists.upsertAsync()`, first our document or modifier will be automatically checked against the schema (in subtly different ways depending on the exact mutator). + +### `defaultValue` and data cleaning + +One thing that Collection2 does is ["clean" the data](https://www.npmjs.com/package/simpl-schema#cleaning-objects) before sending it to the database. This includes but is not limited to: + +1. Coercing types - converting strings to numbers +2. Removing attributes not in the schema +3. Assigning default values based on the `defaultValue` in the schema definition + +However, sometimes it's useful to do more complex initialization to documents before inserting them into collections. For instance, in the Todos app, we want to set the name of new lists to be `List X` where `X` is the next available unique letter. + +To do so, we can subclass `Mongo.Collection` and write our own `insertAsync()` method: + +```js +class ListsCollection extends Mongo.Collection { + async insertAsync(list, options) { + if (!list.name) { + let nextLetter = 'A'; + list.name = `List ${nextLetter}`; + + while (await this.findOneAsync({ name: list.name })) { + // not going to be too smart here, can't go past Z + nextLetter = String.fromCharCode(nextLetter.charCodeAt(0) + 1); + list.name = `List ${nextLetter}`; + } + } + + // Call the original `insertAsync` method, which will validate + // against the schema + return super.insertAsync(list, options); + } +} + +const Lists = new ListsCollection('lists'); +``` + +### Hooks on insert/update/remove + +The technique above can also be used to provide a location to "hook" extra functionality into the collection. For instance, when removing a list, we *always* want to remove all of its todos at the same time. + +We can use a subclass for this case as well, overriding the `removeAsync()` method: + +```js +class ListsCollection extends Mongo.Collection { + // ... + async removeAsync(selector, options) { + await Todos.removeAsync({ listId: selector }); + return super.removeAsync(selector, options); + } +} +``` + +This technique has a few disadvantages: + +1. Mutators can get very long when you want to hook in multiple times. +2. Sometimes a single piece of functionality can be spread over multiple mutators. +3. It can be a challenge to write a hook in a completely general way (that covers every possible selector and modifier), and it may not be necessary for your application (because perhaps you only ever call that mutator in one way). + +A way to deal with points 1. and 2. is to separate out the set of hooks into their own module, and use the mutator as a point to call out to that module in a sensible way. We'll see an example of that [below](#abstracting-denormalizers). + +Point 3. can usually be resolved by placing the hook in the *Method* that calls the mutator, rather than the hook itself. Although this is an imperfect compromise (as we need to be careful if we ever add another Method that calls that mutator in the future), it is better than writing a bunch of code that is never actually called (which is guaranteed to not work!), or giving the impression that your hook is more general that it actually is. + +### Abstracting denormalizers + +Denormalization may need to happen on various mutators of several collections. Therefore, it's sensible to define the denormalization logic in one place, and hook it into each mutator with one line of code. The advantage of this approach is that the denormalization logic is one place rather than spread over many files, but you can still examine the code for each collection and fully understand what happens on each update. + +In the Todos example app, we build an `incompleteCountDenormalizer` to abstract the counting of incomplete todos on the lists. This code needs to run whenever a todo item is inserted, updated (checked or unchecked), or removed. The code looks like: + +```js +const incompleteCountDenormalizer = { + async _updateList(listId) { + // Recalculate the correct incomplete count direct from MongoDB + const incompleteCount = await Todos.find({ + listId, + checked: false + }).countAsync(); + + await Lists.updateAsync(listId, { $set: { incompleteCount } }); + }, + + async afterInsertTodo(todo) { + await this._updateList(todo.listId); + }, + + async afterUpdateTodo(selector, modifier) { + // We only support very limited operations on todos + check(modifier, { $set: Object }); + + // We can only deal with $set modifiers, but that's all we do in this app + if (_.has(modifier.$set, 'checked')) { + const todos = await Todos.find(selector, { fields: { listId: 1 } }).fetchAsync(); + for (const todo of todos) { + await this._updateList(todo.listId); + } + } + }, + + // Here we need to take the list of todos being removed, selected *before* the update + // because otherwise we can't figure out the relevant list id(s) (if the todo has been deleted) + async afterRemoveTodos(todos) { + for (const todo of todos) { + await this._updateList(todo.listId); + } + } +}; +``` + +We are then able to wire in the denormalizer into the mutations of the `Todos` collection like so: + +```js +class TodosCollection extends Mongo.Collection { + async insertAsync(doc, options) { + doc.createdAt = doc.createdAt || new Date(); + const result = await super.insertAsync(doc, options); + await incompleteCountDenormalizer.afterInsertTodo(doc); + return result; + } +} +``` + +Note that we only handled the mutators we actually use in the application—we don't deal with all possible ways the todo count on a list could change. For example, if you changed the `listId` on a todo item, it would need to change the `incompleteCount` of *two* lists. However, since our application doesn't do this, we don't handle it in the denormalizer. + +Dealing with every possible MongoDB operator is difficult to get right, as MongoDB has a rich modifier language. Instead we focus on dealing with the modifiers we know we'll see in our app. If this gets too tricky, then moving the hooks for the logic into the Methods that actually make the relevant modifications could be sensible (although you need to be diligent to ensure you do it in *all* the relevant places, both now and as the app changes in the future). + +## Migrating to a new schema {#migrations} + +As we discussed above, trying to predict all future requirements of your data schema ahead of time is impossible. Inevitably, as a project matures, there will come a time when you need to change the schema of the database. You need to be careful about how you make the migration to the new schema to make sure your app works smoothly during and after the migration. + +### Writing migrations + +A useful package for writing migrations is [`percolate:migrations`](https://atmospherejs.com/percolate/migrations), which provides a nice framework for switching between different versions of your schema. + +Suppose, as an example, that we wanted to add a `list.todoCount` field, and ensure that it was set for all existing lists. Then we might write the following in server-only code (e.g. `/server/migrations.js`): + +```js +import { Migrations } from 'meteor/percolate:migrations'; + +Migrations.add({ + version: 1, + async up() { + const lists = await Lists.find({ todoCount: { $exists: false } }).fetchAsync(); + for (const list of lists) { + const todoCount = await Todos.find({ listId: list._id }).countAsync(); + await Lists.updateAsync(list._id, { $set: { todoCount } }); + } + }, + async down() { + await Lists.updateAsync({}, { $unset: { todoCount: true } }, { multi: true }); + } +}); +``` + +This migration, which is sequenced to be the first migration to run over the database, will, when called, bring each list up to date with the current todo count. + +To find out more about the API of the Migrations package, refer to [its documentation](https://atmospherejs.com/percolate/migrations). + +### Bulk changes + +If your migration needs to change a lot of data, and especially if you need to stop your app server while it's running, it may be a good idea to use a [MongoDB Bulk Operation](https://docs.mongodb.org/v3.0/core/bulk-write-operations/). + +The advantage of a bulk operation is that it only requires a single round trip to MongoDB for the write, which usually means it is a *lot* faster. The downside is that if your migration is complex (which it usually is if you can't do an `.updateAsync(.., .., {multi: true})`), it can take a significant amount of time to prepare the bulk update. + +What this means is if users are accessing the site whilst the update is being prepared, it will likely go out of service! Also, a bulk update will lock the entire collection while it is being applied, which can cause a significant blip in your user experience if it takes a while. For these reasons, you often need to stop your server and let your users know you are performing maintenance while the update is happening. + +We could write our above migration like so. We can access the native MongoDB API via [`Collection#rawCollection()`](/api/collections#Mongo-Collection-rawCollection): + +```js +Migrations.add({ + version: 1, + async up() { + // This is how to get access to the raw MongoDB node collection + const rawCollection = Lists.rawCollection(); + + const lists = await Lists.find({ todoCount: { $exists: false } }).fetchAsync(); + + if (lists.length === 0) { + return true; + } + + const operations = []; + for (const list of lists) { + const todoCount = await Todos.find({ listId: list._id }).countAsync(); + operations.push({ + updateOne: { + filter: { _id: list._id }, + update: { $set: { todoCount } } + } + }); + } + + if (operations.length > 0) { + await rawCollection.bulkWrite(operations); + } + + return true; + }, + async down() { + await Lists.updateAsync({}, { $unset: { todoCount: true } }, { multi: true }); + } +}); +``` + +Note that we could make this migration faster by using an [Aggregation](https://docs.mongodb.org/v3.4/aggregation/) to gather the initial set of todo counts. + +### Running migrations + +To run a migration against your development database, it's easiest to use the Meteor shell: + +```js +// After running `meteor shell` on the command line: +await Migrations.migrateTo('latest'); +``` + +If the migration logs anything to the console, you'll see it in the terminal window that is running the Meteor server. + +To run a migration against your production database, run your app locally in production mode (with production settings and environment variables, including database settings), and use the Meteor shell in the same way. What this does is run the `up()` function of all outstanding migrations, against your production database. In our case, it should ensure all lists have a `todoCount` field set. + +A good way to do the above is to spin up a virtual machine close to your database that has Meteor installed and SSH access (a special EC2 instance that you start and stop for the purpose is a reasonable option), and running the command after shelling into it. That way any latencies between your machine and the database will be eliminated, but you still can be very careful about how the migration is run. + +::: warning +You should always make a database backup before running any migration! +::: + +### Breaking schema changes + +Sometimes when we change the schema of an application, we do so in a breaking way—so that the old schema doesn't work properly with the new code base. For instance, if we had some UI code that heavily relied on all lists having a `todoCount` set, there would be a period, before the migration runs, in which the UI of our app would be broken after we deployed. + +The simple way to work around the problem is to take the application down for the period in between deployment and completing the migration. This is far from ideal, especially considering some migrations can take hours to run (although using [Bulk Updates](#bulk-changes) probably helps a lot here). + +A better approach is a multi-stage deployment. The basic idea is that: + +1. Deploy a version of your application that can handle both the old and the new schema. In our case, it'd be code that doesn't expect the `todoCount` to be there, but which correctly updates it when new todos are created. +2. Run the migration. At this point you should be confident that all lists have a `todoCount`. +3. Deploy the new code that relies on the new schema and no longer knows how to deal with the old schema. Now we are safe to rely on `list.todoCount` in our UI. + +Another thing to be aware of, especially with such multi-stage deploys, is that being prepared to rollback is important! For this reason, the migrations package allows you to specify a `down()` function and call `Migrations.migrateTo(x)` to migrate *back* to version `x`. + +So if we wanted to reverse our migration above, we'd run: + +```js +// The "0" migration is the unmigrated (before the first migration) state +await Migrations.migrateTo(0); +``` + +If you find you need to roll your code version back, you'll need to be careful about the data, and step carefully through your deployment steps in reverse. + +### Migration caveats + +Some aspects of the migration strategy outlined above are possibly not the most ideal way to do things (although perhaps appropriate in many situations). Here are some other things to be aware of: + +1. Usually it is better to not rely on your application code in migrations (because the application will change over time, and the migrations should not). For instance, having your migrations pass through your Collection2 collections (and thus check schemas, set autovalues etc) is likely to break them over time as your schemas change over time. + + One way to avoid this problem is to not run old migrations on your database. This is a little bit limiting but can be made to work. + +2. Running the migration on your local machine will probably make it take a lot longer as your machine isn't as close to the production database as it could be. + +Deploying a special "migration application" to the same hardware as your real application is probably the best way to solve the above issues. It'd be amazing if such an application kept track of which migrations ran when, with logs and provided a UI to examine and run them. + +## Associations between collections + +As we discussed earlier, it's very common in Meteor applications to have associations between documents in different collections. Consequently, it's also very common to need to write queries fetching related documents once you have a document you are interested in (for instance all the todos that are in a single list). + +To make this easier, we can attach functions to the prototype of the documents that belong to a given collection, to give us "methods" on the documents (in the object oriented sense). We can then use these methods to create new queries to find related documents. + +### Using Grapher for associations + +To make things easier, we can use the [`cultofcoders:grapher`](https://atmospherejs.com/cultofcoders/grapher) package to associate collections and fetch their relations. For example: + +```js +// Configure how collections relate to each other +Todos.addLinks({ + list: { + type: 'one', + field: 'listId', + collection: Lists + } +}); + +Lists.addLinks({ + todos: { + collection: Todos, + inversedBy: 'list' // This represents the name of the link we defined in Todos + } +}); +``` + +This allows us to properly fetch a list along with its todos: + +```js +// With Grapher you must always specify the fields you want +const listsAndTodos = await Lists.createQuery({ + name: 1, + todos: { + text: 1 + } +}).fetchAsync(); +``` + +`listsAndTodos` will look like this: + +```js +[ + { + name: 'My List', + todos: [ + { text: 'Do something' } + ], + } +] +``` + +Grapher supports isomorphic queries (reactive and non-reactive), has built-in security features, works with many types of relationships, and more. Refer to the [Grapher documentation](https://github.com/cult-of-coders/grapher/blob/master/docs/index.md) for more details. + +### Collection helpers + +We can use the [`dburles:collection-helpers`](https://atmospherejs.com/dburles/collection-helpers) package to easily attach such methods (or "helpers") to documents. For instance: + +```js +Lists.helpers({ + // A list is considered to be private if it has a userId set + isPrivate() { + return !!this.userId; + } +}); +``` + +Once we've attached this helper to the `Lists` collection, every time we fetch a list from the database (on the client or server), it will have a `.isPrivate()` function available: + +```js +const list = await Lists.findOneAsync(); +if (list.isPrivate()) { + console.log('The first list is private!'); +} +``` + +### Association helpers + +Now we can attach helpers to documents, we can define a helper that fetches related documents: + +```js +Lists.helpers({ + todos() { + return Todos.find({ listId: this._id }, { sort: { createdAt: -1 } }); + }, + + async todosAsync() { + return Todos.find({ listId: this._id }, { sort: { createdAt: -1 } }).fetchAsync(); + } +}); +``` + +Now we can find all the todos for a list: + +```js +const list = await Lists.findOneAsync(); +const todoCount = await list.todos().countAsync(); +console.log(`The first list has ${todoCount} todos`); +``` + +## Best practices summary + +Here's a summary of best practices when working with collections and schemas: + +1. **Always define schemas** for your collections using `simpl-schema` or `jagi:astronomy`. + +2. **Use Collection2** to automatically validate documents on write operations. + +3. **Consider DDP efficiency** when designing your schema—avoid large nested arrays that change frequently. + +4. **Denormalize thoughtfully** to avoid expensive joins while keeping data consistent. + +5. **Use migrations** for schema changes, and always test them on staging first. + +6. **Backup your database** before running any migration in production. + +7. **Use async methods** (`insertAsync`, `updateAsync`, `findOneAsync`, etc.) throughout your Meteor 3 application. + +8. **Abstract denormalization logic** into reusable modules to keep your code DRY. + +9. **Design for the future** but don't over-engineer—add fields when you actually need them. diff --git a/v3-docs/docs/tutorials/data-loading/data-loading.md b/v3-docs/docs/tutorials/data-loading/data-loading.md new file mode 100644 index 0000000000..fe5ad13817 --- /dev/null +++ b/v3-docs/docs/tutorials/data-loading/data-loading.md @@ -0,0 +1,417 @@ +# Publications and Data Loading + +After reading this guide, you'll know: + +1. What publications and subscriptions are in Meteor. +2. How to define a publication on the server. +3. Where to subscribe on the client and in which components. +4. Useful patterns for managing subscriptions. +5. How to reactively publish related data. +6. How to ensure your publication is secure. +7. How to use the low-level publish API. + +## Publications and subscriptions + +In a traditional, HTTP-based web application, the client and server communicate in a "request-response" fashion. Typically the client makes RESTful HTTP requests to the server and receives HTML or JSON data in response, and there's no way for the server to "push" data to the client when changes happen at the backend. + +Meteor is built from the ground up on the Distributed Data Protocol (DDP) to allow data transfer in both directions. Instead of setting up REST endpoints, you create *publication* endpoints that can push data from server to client. + +In Meteor a **publication** is a named API on the server that constructs a set of data to send to a client. A client initiates a **subscription** which connects to a publication, and receives that data. That data consists of a first batch sent when the subscription is initialized and then incremental updates as the published data changes. + +A subscription "bridges" a server-side MongoDB collection and the client-side Minimongo cache of that collection. You can think of a subscription as a pipe that connects a subset of the "real" collection with the client's version, and constantly keeps it up to date with the latest information on the server. + +## Defining a publication + +A publication should be defined in a server-only file. For instance, in the Todos example app, we want to publish the set of public lists to all users: + +```js +import { Meteor } from 'meteor/meteor'; +import { Lists } from '/imports/api/lists/lists'; + +Meteor.publish('lists.public', function() { + return Lists.find({ + userId: { $exists: false } + }, { + fields: Lists.publicFields + }); +}); +``` + +There are a few things to understand about this code block. First, we've named the publication with the unique string `lists.public`, and that will be how we access it from the client. Second, we are returning a Mongo *cursor* from the publication function. Note that the cursor is filtered to only return certain fields from the collection. + +What that means is that the publication will ensure the set of data matching that query is available to any client that subscribes to it. + +Every publication takes two types of parameters: + +1. The `this` context, which has information about the current DDP connection. For example, you can access the current user's `_id` with `this.userId`. +2. The arguments to the publication, which can be passed in when calling `Meteor.subscribe`. + +::: info +Since we need to access context on `this` we need to use the `function() {}` form for publications rather than the ES2015 `() => {}` arrow function. +::: + +Here's a publication that loads private lists for the current user: + +```js +Meteor.publish('lists.private', function() { + if (!this.userId) { + return this.ready(); + } + + return Lists.find({ + userId: this.userId + }, { + fields: Lists.publicFields + }); +}); +``` + +Thanks to the guarantees provided by DDP and Meteor's accounts system, this publication will only ever publish private lists to the user that they belong to. The publication will re-run if the user logs out (or back in again), which means that the published set of private lists will change as the active user changes. + +In the case of a logged-out user, we explicitly call `this.ready()`, which indicates to the subscription that we've sent all the data we are initially going to send. It's important to know that if you don't return a cursor from the publication or call `this.ready()`, the user's subscription will never become ready. + +### Publications with arguments + +Here's an example of a publication which takes a named argument: + +```js +import SimpleSchema from 'simpl-schema'; + +Meteor.publish('todos.inList', function(listId) { + // Validate the argument + new SimpleSchema({ + listId: { type: String } + }).validate({ listId }); + + if (!this.userId) { + return this.ready(); + } + + return Todos.find({ listId }); +}); +``` + +When we subscribe to this publication on the client, we can provide this argument: + +```js +Meteor.subscribe('todos.inList', list._id); +``` + +## Subscribing to data + +To use publications, you need to create a subscription to it on the client. To do so, you call `Meteor.subscribe()` with the name of the publication: + +```js +const handle = Meteor.subscribe('lists.public'); +``` + +`Meteor.subscribe()` returns a "subscription handle" with these important properties: + +- `.ready()` - A reactive function that returns `true` when the publication is marked ready +- `.stop()` - Stops the subscription and clears the data from the client cache + +### Stopping subscriptions + +When you are subscribing, it is very important to ensure that you always call `.stop()` on the subscription when you are done with it. This ensures that the documents sent by the subscription are cleared from your local Minimongo cache and the server stops doing the work required to service your subscription. + +*However*, if you call `Meteor.subscribe()` inside a reactive context (such as a `Tracker.autorun`), then Meteor's reactive system will automatically call `.stop()` for you at the appropriate time. + +### Subscribe in React components + +Here's how to subscribe in a React component using the `useSubscribe` hook from `react-meteor-data`: + +```jsx +import { useSubscribe, useFind } from 'meteor/react-meteor-data'; +import { Lists } from '/imports/api/lists/lists'; + +function ListsPage() { + const isLoading = useSubscribe('lists.public'); + const lists = useFind(() => Lists.find(), []); + + if (isLoading()) { + return
    Loading...
    ; + } + + return ( +
      + {lists.map(list => ( +
    • {list.name}
    • + ))} +
    + ); +} +``` + +For subscriptions with arguments: + +```jsx +function TodosPage({ listId }) { + const isLoading = useSubscribe('todos.inList', listId); + const todos = useFind(() => Todos.find({ listId }), [listId]); + + if (isLoading()) { + return
    Loading...
    ; + } + + return ( +
      + {todos.map(todo => ( +
    • {todo.text}
    • + ))} +
    + ); +} +``` + +### Subscribe in Blaze templates + +In Blaze, it's best to subscribe in the `onCreated()` callback: + +```js +Template.Lists_show_page.onCreated(function() { + this.getListId = () => FlowRouter.getParam('_id'); + + this.autorun(() => { + this.subscribe('todos.inList', this.getListId()); + }); +}); +``` + +Calling `this.subscribe()` (rather than `Meteor.subscribe`) attaches a special `subscriptionsReady()` function to the template instance, which is true when all subscriptions made inside this template are ready. + +## Fetching data + +Subscribing to data puts it in your client-side collection. To use the data in your user interface, you need to query your client-side collection. + +### Always use specific queries + +If you're publishing a subset of your data, always re-specify the query when fetching data on the client: + +```js +// Good - specific query +const publicLists = Lists.find({ userId: { $exists: false } }); + +// Avoid - too broad, might include data from other subscriptions +const allLists = Lists.find(); +``` + +### Fetch near where you subscribed + +Place the fetch logic close to where you subscribe to avoid action at a distance and make data flow easier to understand: + +```jsx +function TodoList({ listId }) { + const isLoading = useSubscribe('todos.inList', listId); + const todos = useFind(() => Todos.find({ listId }), [listId]); + + // Both subscription and fetch are in the same component + if (isLoading()) return ; + return ; +} +``` + +## Patterns for data loading + +### Subscription readiness + +It is key to understand that a subscription will not instantly provide its data. There will be a latency between subscribing to the data on the client and it arriving from the publication on the server. + +```js +const handle = Meteor.subscribe('lists.public'); + +Tracker.autorun(() => { + const isReady = handle.ready(); + console.log(`Handle is ${isReady ? 'ready' : 'not ready'}`); +}); +``` + +For multiple subscriptions: + +```js +const handles = [ + Meteor.subscribe('lists.public'), + Meteor.subscribe('todos.inList', listId), +]; + +Tracker.autorun(() => { + const areReady = handles.every(handle => handle.ready()); + console.log(`Handles are ${areReady ? 'ready' : 'not ready'}`); +}); +``` + +### Reactively changing subscription arguments + +When using React hooks or Blaze autoruns, subscriptions will automatically re-run when their reactive dependencies change: + +```jsx +function TodoList({ listId }) { + // Subscription will automatically re-run when listId changes + const isLoading = useSubscribe('todos.inList', listId); + // ... +} +``` + +### Pagination + +Here's a pattern for paginated subscriptions: + +```js +const PAGE_SIZE = 20; + +Meteor.publish('todos.paginated', function(listId, page = 1) { + new SimpleSchema({ + listId: { type: String }, + page: { type: Number, min: 1 } + }).validate({ listId, page }); + + const skip = (page - 1) * PAGE_SIZE; + + return Todos.find({ listId }, { + sort: { createdAt: -1 }, + skip, + limit: PAGE_SIZE + }); +}); +``` + +And on the client: + +```jsx +function PaginatedTodos({ listId }) { + const [page, setPage] = useState(1); + const isLoading = useSubscribe('todos.paginated', listId, page); + + // ... +} +``` + +## Publishing related data + +Sometimes you need to publish data from multiple collections that are related. There are two main approaches: + +### Multiple cursors + +You can return an array of cursors from a publication: + +```js +Meteor.publish('lists.withTodos', function(listId) { + return [ + Lists.find({ _id: listId }), + Todos.find({ listId }) + ]; +}); +``` + +### Reactive joins with reywood:publish-composite + +For more complex relationships, use the `reywood:publish-composite` package: + +```bash +meteor add reywood:publish-composite +``` + +```js +import { publishComposite } from 'meteor/reywood:publish-composite'; + +publishComposite('lists.withTodosAndAuthors', function(listId) { + return { + find() { + return Lists.find({ _id: listId }); + }, + children: [{ + find(list) { + return Todos.find({ listId: list._id }); + }, + children: [{ + find(todo) { + return Meteor.users.find({ _id: todo.authorId }, { + fields: { profile: 1, username: 1 } + }); + } + }] + }] + }; +}); +``` + +## Low-level publish API + +For more control over what gets published, you can use the low-level publish API: + +```js +Meteor.publish('custom.publication', function() { + const self = this; + + // Add a document + self.added('collection-name', 'document-id', { field: 'value' }); + + // Change a document + self.changed('collection-name', 'document-id', { field: 'new-value' }); + + // Remove a document + self.removed('collection-name', 'document-id'); + + // Signal that initial data has been sent + self.ready(); + + // Clean up on stop + self.onStop(() => { + // cleanup code + }); +}); +``` + +This is useful for: +- Publishing data from external sources +- Custom aggregation results +- Real-time data from non-MongoDB sources + +## Security considerations + +### Always validate arguments + +```js +Meteor.publish('todos.inList', function(listId) { + // Always validate + check(listId, String); + // or use SimpleSchema + new SimpleSchema({ + listId: { type: String } + }).validate({ listId }); + + // ... +}); +``` + +### Filter fields + +Don't publish sensitive fields: + +```js +Meteor.publish('users.public', function() { + return Meteor.users.find({}, { + fields: { + username: 1, + profile: 1 + // Don't include emails, services, etc. + } + }); +}); +``` + +### Check authorization + +Make sure users can only access data they're authorized to see: + +```js +Meteor.publish('lists.private', function() { + if (!this.userId) { + return this.ready(); + } + + // Only publish lists the user owns + return Lists.find({ userId: this.userId }); +}); +``` + +See the [Security article](/tutorials/security/security) for more details on securing publications. diff --git a/v3-docs/docs/tutorials/deployment/deployment.md b/v3-docs/docs/tutorials/deployment/deployment.md new file mode 100644 index 0000000000..d0c026661d --- /dev/null +++ b/v3-docs/docs/tutorials/deployment/deployment.md @@ -0,0 +1,420 @@ +# Deployment and Monitoring + +After reading this guide, you'll know: + +1. What to consider before you deploy a Meteor application. +2. How to deploy to common Meteor hosting environments. +3. How to design a deployment process to make sure your application's quality is maintained. +4. How to monitor user behavior with analytics tools. +5. How to monitor your application. +6. How to make sure your site is discoverable by search engines. + +## Deploying Meteor Applications + +Once you've built and tested your Meteor application, you need to put it online to show it to the world. Deploying a Meteor application is similar to deploying any other websocket-based Node.js app, but is different in some of the specifics. + +Deploying a web application is fundamentally different from releasing most other kinds of software, in that you can deploy as often as you'd like to. You don't need to wait for users to do something to get the new version of your software because the server will push it right at them. + +However, it's still important to test your changes thoroughly with a good process of Quality Assurance (QA). Although it's easy to push out fixes to bugs, those bugs can still cause major problems to users and even potentially data corruption! + +::: danger Never use `--production` flag to deploy! +`--production` flag is purely meant to simulate production minification, but does almost nothing else. This still watches source code files, exchanges data with package server and does a lot more than just running the app, leading to unnecessary computing resource wasting and security issues. Please don't use `--production` flag to deploy! +::: + +### Deployment environments + +In web application deployment it's common to refer to three runtime environments: + +1. **Development.** This refers to your machine where you develop new features and run local tests. +2. **Staging.** An intermediate environment that is similar to production, but not visible to users of the application. Can be used for testing and QA. +3. **Production.** The real deployment of your app that your customers are currently using. + +The idea of the staging environment is to provide a non-user-visible test environment that is as close as possible to production in terms of infrastructure. It's common for issues to appear with new code on the production infrastructure that don't happen in a development environment. A very simple example is issues that involve latency between the client and server---connecting to a local development server with tiny latencies, you just may never see such an issue. + +For this reason, developers tend to try and get staging as close as possible to production. This means that all the steps we outline below about production deployment, should, if possible, also be followed for your staging server. + +### Environment variables and settings + +There are two main ways to configure your application outside of the code of the app itself: + +1. **Environment variables.** This is the set of `ENV_VARS` that are set on the running process. +2. **Settings.** These are in a JSON object set via either the `--settings` Meteor command-line flag or stringified into the `METEOR_SETTINGS` environment variable. + +Settings should be used to set environment (i.e. staging vs production) specific things, like the access token and secret used to connect to Google. These settings will not change between any given process running your application in the given environment. + +Environment variables are used to set process-specific things, which could conceivably change for different instances of your application's processes. A list of environment variables can be found in the [CLI documentation](/cli/environment-variables). + +A final note on storing these settings: It's not a good idea to store settings in the same repository where you keep your app code. Read about good places to put your settings in the [Security article](/tutorials/security/security#api-keys). + +## Other considerations + +There are some other considerations that you should make before you deploy your application to a production host. Remember that you should if possible do these steps for both your production *and* staging environments. + +### Domain name + +What URL will users use to access your site? You'll probably need to register a domain name with a domain registrar, and setup DNS entries to point to the site (this will depend on how you deploy, see below). + +### SSL Certificate + +It's always a good idea to use SSL for Meteor applications (see the [Security Article](/tutorials/security/security#ssl) to find out why). Once you have a registered domain name, you'll need to generate an SSL certificate with a certificate authority for your domain. Many hosting providers offer free SSL certificates through Let's Encrypt. + +### CDN + +It's not strictly required, but often a good idea to set up a Content Delivery Network (CDN) for your site. A CDN is a network of servers that hosts the static assets of your site (such as JavaScript, CSS, and images) in numerous locations around the world and uses the server closest to your user to provide those files in order to speed up their delivery. + +For Meteor, we recommend using a CDN with "origin" support (like CloudFront), which means that instead of uploading your files in advance, the CDN automatically fetches them from your server. You put your files in `public/`, and when your user asks the CDN for a file, the CDN fetches it from your server behind the scenes. + +To get Meteor to use the CDN for your JavaScript and CSS bundles, call `WebAppInternals.setBundledJsCssPrefix("https://mycdn.com")` on the server. This will also take care of relative image URLs inside your CSS files. + +For all your files in `public/`, change their URLs to point at the CDN. You can use a helper like `assetUrl`: + +```js +// Register a global helper for asset URLs +Template.registerHelper("assetUrl", (asset) => { + return "https://mycdn.com/" + asset; +}); +``` + +```html + +``` + +#### CDNs and webfonts + +If you are hosting a webfont as part of your application and serving it via a CDN, you may need to configure the served headers for the font to allow cross-origin resource sharing: + +```js +import { WebApp } from 'meteor/webapp'; + +WebApp.rawHandlers.use(function(req, res, next) { + if (req._parsedUrl.pathname.match(/\.(ttf|ttc|otf|eot|woff|woff2|font\.css|css)$/)) { + res.setHeader('Access-Control-Allow-Origin', '*'); + } + next(); +}); +``` + +## Deployment options + +Meteor is an open source platform, and you can run the apps that you make with Meteor anywhere just like regular Node.js applications. But operating Meteor apps *correctly*, so that your apps work for everyone, can be tricky if you are managing your infrastructure manually. + +### Galaxy Cloud (Recommended) + +[Galaxy Cloud](https://galaxycloud.app) is a service built specifically to run Meteor apps. It's the easiest way to operate your app with confidence. + +Galaxy is a distributed system that runs on cloud infrastructure. If you understand what it takes to run Meteor apps correctly and how Galaxy works, you'll come to appreciate Galaxy's value, and that it will save you a lot of time and trouble. + +In order to deploy to Galaxy, you'll need to sign up for an account and separately provision a MongoDB database (see below). + +Once you've done that, deployment is straightforward. You need to add some environment variables to your settings file to point it at your MongoDB, and you can deploy with: + +```bash +DEPLOY_HOSTNAME=us-east-1.galaxy.meteor.com meteor deploy your-app.com --settings production-settings.json +``` + +You can also log into the Galaxy UI to manage your applications, monitor the number of connections and resource usage, view logs, and change settings. + +### Meteor Up + +[Meteor Up](https://meteor-up.com), often referred to as "mup", is a third-party, open-source tool that can be used to deploy Meteor applications to any online server over SSH. It's essentially a way to automate the manual steps of using `meteor build` and putting that bundle on your server. + +You can obtain a server running Ubuntu or Debian from many generic hosting providers and Meteor Up can SSH into your server with the keys you provide in the config. You can get started with the [tutorial](https://meteor-up.com/getting-started.html). + +One of its plugins, [mup-aws-beanstalk](https://github.com/zodern/mup-aws-beanstalk/) deploys Meteor Apps to AWS Elastic Beanstalk instead of a server. It supports autoscaling, load balancing, and zero downtime deploys. + +### Docker + +To orchestrate your own container-based deployment there are existing base images to consider: + +- [Official Node.js images](https://hub.docker.com/_/node) - Use as a base for your custom Dockerfile +- [meteor/meteor-base](https://hub.docker.com/r/meteor/meteor-base) - Official Meteor base images + +Here's a basic Dockerfile example for a Meteor 3 application: + +```dockerfile +# Build stage +FROM node:20-alpine AS builder + +RUN apk add --no-cache python3 make g++ git + +WORKDIR /app +COPY . . + +# Install Meteor +RUN curl https://install.meteor.com/ | sh + +# Build the application +RUN meteor npm install --production +RUN meteor build --directory /build --server-only + +# Production stage +FROM node:20-alpine + +WORKDIR /app + +# Copy built application +COPY --from=builder /build/bundle . + +# Install production dependencies +WORKDIR /app/programs/server +RUN npm install --production + +WORKDIR /app + +ENV PORT=3000 +ENV ROOT_URL=http://localhost:3000 + +EXPOSE 3000 + +CMD ["node", "main.js"] +``` + +### Custom deployment + +If you want to figure out your hosting solution completely from scratch, the Meteor tool has a command `meteor build` that creates a deployment bundle that contains a plain Node.js application. Any npm dependencies must be installed before issuing the `meteor build` command to be included in the bundle. + +**NOTE:** It's important that you build your bundle for the correct architecture: + +```bash +# for example if deploying to a Ubuntu linux server: +npm install --production +meteor build /path/to/build --architecture os.linux.x86_64 +``` + +This will provide you with a bundled application `.tar.gz` which you can extract and run without the `meteor` tool. The environment you choose will need the correct version of Node.js and connectivity to a MongoDB server. + +To find out which version of Node you should use, run `meteor node -v` in the development environment, or check the `.node_version.txt` file within the bundle. For Meteor 3.x, you'll need Node.js 20.x. + +::: warning +If you use a mis-matched version of Node when deploying your application, you will encounter errors! +::: + +You can then run the application by invoking `node` with environment variables: + +```bash +cd my_build_bundle_directory +(cd programs/server && npm install) +MONGO_URL=mongodb://localhost:27017/myapp ROOT_URL=http://my-app.com PORT=3000 node main.js +``` + +- `ROOT_URL` is the base URL for your Meteor project +- `PORT` is the port at which the application is running +- `MONGO_URL` is a [Mongo connection string URI](https://docs.mongodb.com/manual/reference/connection-string/) + +## MongoDB options + +When you deploy your Meteor server, you need a `MONGO_URL` that points to your MongoDB database. You can either use a hosted MongoDB service or set up and run your own MongoDB server. We recommend using a hosted service, as the time saved and peace of mind are usually worth the cost. + +### Hosted service (Recommended) + +There are a variety of services out there: + +- [MongoDB Atlas](https://www.mongodb.com/cloud/atlas) - The official MongoDB cloud service +- [DigitalOcean Managed Databases](https://www.digitalocean.com/products/managed-databases-mongodb) +- [AWS DocumentDB](https://aws.amazon.com/documentdb/) (MongoDB compatible) + +When selecting a hosted MongoDB service for production, consider: + +- Supports the MongoDB version you wish to run +- Storage Engine Support (WiredTiger is default since Meteor 1.4) +- Support for Replica Sets & Oplog tailing +- Monitoring & Automated alerting +- Continuous backups & Automated snapshots +- Access Control, IP whitelisting, and VPC Peering +- Encryption of data in-flight and at-rest +- Cost and pricing granularity +- Instance size & options + +### Self-hosted + +You can install MongoDB on your own server. As you can see from the above section, there are many aspects of database setup and maintenance that you have to take care of. For the best performance, choose a server with an SSD large enough to fit your data and with enough RAM to fit the working set (indexes + active documents) in memory. + +## Deployment process + +Although it's much easier to deploy a web application than release most other types of software, that doesn't mean you should be cavalier with your deployment. It's important to properly QA and test your releases before you push them live. + +A typical release process looks like: + +1. Deploy the new version of the application to your staging server. +2. QA the application on the staging server. +3. Fix any bugs found in step 2 and repeat. +4. Once you are satisfied with the staging release, release the *exact same* version to production. +5. Run final QA on production. + +Steps 2 and 5 can be time-consuming, especially if you are aiming to maintain a high level of quality. That's why it's a great idea to develop a suite of acceptance tests (see our [Testing Article](/tutorials/testing/testing) for more on this). + +### Continuous deployment + +Continuous deployment refers to the process of deploying an application via a continuous integration tool, usually when some condition is reached (such as a git push to the `main` branch). + +Here's an example GitHub Actions workflow for deploying to Galaxy: + +```yaml +name: Deploy to Galaxy + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Meteor + run: curl https://install.meteor.com/ | sh + + - name: Deploy to Galaxy + env: + METEOR_SESSION_FILE: ${{ secrets.METEOR_SESSION_FILE }} + run: | + echo "$METEOR_SESSION_FILE" > meteor-session.json + METEOR_SESSION_FILE=meteor-session.json \ + DEPLOY_HOSTNAME=us-east-1.galaxy.meteor.com \ + meteor deploy your-app.com --settings settings.json +``` + +### Rolling deployments and data versions + +It's important to understand what happens during a deployment, especially if your deployment involves changes in data format (and potentially data migrations, see the [Collections Article](/tutorials/collections/collections#migrations)). + +When you are running your app on multiple servers or containers, it's not a good idea to shut down all of the servers at once and then start them all back up again. This will result in more downtime than necessary, and will cause a huge spike in CPU usage when all of your clients reconnect again at the same time. Modern hosting platforms stop and re-start containers one by one during deployment. + +If the new version involves different data formats in the database, then you need to be careful about how you step through versions to ensure that all the versions that are running simultaneously can work together. + +## Monitoring users via analytics {#analytics} + +It's common to want to know which pages of your app are most commonly visited, and where users are coming from. Here's a setup using Google Analytics: + +```js +// client/analytics.js +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; + +// Initialize Google Analytics +window.dataLayer = window.dataLayer || []; +function gtag() { dataLayer.push(arguments); } +gtag('js', new Date()); +gtag('config', 'G-XXXXXXXXXX'); // Your GA4 Measurement ID + +// Track page views on route changes +FlowRouter.triggers.enter([() => { + gtag('event', 'page_view', { + page_path: FlowRouter.current().path, + page_title: document.title + }); +}]); +``` + +Add the Google Analytics script to your ``: + +```html + + + +``` + +You may want to track non-page change related events (for instance method calls) also: + +```js +export const updateText = { + async run({ todoId, newText }) { + if (Meteor.isClient) { + gtag('event', 'todos_update_text', { + todo_id: todoId, + custom_parameter: 'value' + }); + } + + // ... method implementation + } +}; +``` + +## Monitoring your application + +When you are running an app in production, it's vitally important that you keep tabs on the performance of your application and ensure it is running smoothly. + +### Understanding Meteor performance + +Although a host of tools exist to monitor the performance of HTTP request-response based applications, the insights they give aren't necessarily useful for a connected client system like a Meteor application. While slow HTTP response times would be a problem, there are many kinds of issues that won't be surfaced by traditional monitoring tools. + +### APM (Application Performance Monitoring) + +If you really want to understand the ins and outs of running your Meteor application, you should use an Application Performance Monitoring (APM) service: + +- [Monti APM](https://montiapm.com/) - Designed specifically for Meteor +- [Meteor Elastic APM](https://github.com/Meteor-Community-Packages/meteor-elastic-apm) - Integration with Elastic APM +- [Datadog APM](https://www.datadoghq.com/product/apm/) - General-purpose APM with custom integrations + +These APMs operate by taking regular client and server side observations of your application's performance and reporting them back to a monitoring server. + +#### Method and Publication Latency + +Rather than monitoring HTTP response times, in a Meteor app it makes far more sense to consider DDP response times. The two actions your client will wait for in terms of DDP are *method calls* and *publication subscriptions*. APM tools help you discover which of your methods and publications are slow and resource intensive. + +#### Livequery Monitoring + +A key performance characteristic of Meteor is driven by the behavior of livequery, the technology that allows your publications to push changing data automatically in realtime. In order to achieve this, livequery needs to monitor your MongoDB instance for changes (by tailing the oplog) and decide if a given change is relevant for the given publication. + +If the publication is used by a lot of users, or there are many changes to be compared, then these livequery observers can do a lot of work. APM tools can help you understand your livequery usage and optimize accordingly. + +## Enabling SEO + +If your application contains a lot of publicly accessible content, then you probably want it to rank well in Google and other search engines' indexes. As most webcrawlers have limited support for client-side rendering, it's better to render the site on the server and deliver it as HTML. + +### Server-Side Rendering + +For React applications, you can use the [`server-render`](/packages/server-render) package to implement server-side rendering: + +```js +import { onPageLoad } from 'meteor/server-render'; +import React from 'react'; +import { renderToString } from 'react-dom/server'; +import App from '/imports/ui/App'; + +onPageLoad(sink => { + const html = renderToString(); + sink.renderIntoElementById('react-root', html); +}); +``` + +### Prerendering Services + +You can also use prerendering services like [Prerender.io](https://prerender.io) to serve pre-rendered HTML to search engine crawlers: + +```js +import { WebApp } from 'meteor/webapp'; +import prerender from 'prerender-node'; + +WebApp.connectHandlers.use( + prerender.set('prerenderToken', 'YOUR_TOKEN') +); +``` + +### Setting Page Metadata + +To set `` tags and other `<head>` content for SEO, you can use React Helmet or similar packages: + +```jsx +import { Helmet } from 'react-helmet'; + +function ProductPage({ product }) { + return ( + <> + <Helmet> + <title>{product.name} - My Store + + + + + {/* Page content */} + + ); +} +``` diff --git a/v3-docs/docs/tutorials/integrations/flowbite.md b/v3-docs/docs/tutorials/integrations/flowbite.md new file mode 100644 index 0000000000..f294921ccf --- /dev/null +++ b/v3-docs/docs/tutorials/integrations/flowbite.md @@ -0,0 +1,225 @@ +# Flowbite UI with Tailwind CSS + +Learn how to install Tailwind CSS with Flowbite for your Meteor.js project to build modern, responsive web applications. + +## Introduction + +[Flowbite](https://flowbite.com/) is an open-source library of UI components based on the utility-first Tailwind CSS framework featuring dark mode support, a Figma design system, templates, and more. + +It includes all of the commonly used components that a website requires, such as buttons, dropdowns, navigation bars, modals, but also some more advanced interactive elements such as datepickers. + +Using both Meteor.js, Tailwind CSS and Flowbite can help you get started building modern UI web applications by leveraging the extensive framework features of Meteor.js, the utility-first approach of the Tailwind CSS framework and the open-source UI components from the Flowbite Library. + +## Requirements + +Make sure that you have [Node.js v14](https://nodejs.org/en/) or newer installed on your computer to be able to install Meteor.js, Tailwind CSS and Flowbite using npm. + +For more information on how to install Meteor.js, check out the [official installation guide](/about/install). + +## Create a new Meteor project with Tailwind + +The easiest way to create a new Meteor.js project with Tailwind CSS support is by using the `--tailwind` flag: + +```bash +meteor create flowbite-app --tailwind +cd flowbite-app +``` + +This will create a new Meteor project with Tailwind CSS pre-configured. No extra configuration needed as we added the `--tailwind` flag when setting up the project. + +Now that you have created a new Meteor.js project with Tailwind CSS configured automatically, we can proceed with installing Flowbite and Flowbite React to start leveraging the open-source UI components. + +## Install Flowbite + +1. Install Flowbite and Flowbite React via npm: + +```bash +npm install --save flowbite flowbite-react +``` + +2. Make sure that you set up the Flowbite plugin and template paths in your `tailwind.config.js` file: + +```js +module.exports = { + content: [ + './imports/ui/**/*.{js,jsx,ts,tsx}', + './client/*.html', + 'node_modules/flowbite-react/**/*.{js,jsx,ts,tsx}', + ], + theme: { + extend: {}, + }, + plugins: [require('flowbite/plugin')], +}; +``` + +3. Now that you have installed the packages you can start importing the UI components: + +```jsx +import { Alert } from 'flowbite-react'; + +export default function MyPage() { + return Alert!; +} +``` + +The code above will import the `` component that you can use to send feedback messages. + +## Example components + +To get you started, here are some examples of how to use Flowbite components in your Meteor.js project. + +### Modal with Button + +```jsx +import { useState } from 'react'; +import { Button, Modal } from 'flowbite-react'; + +export default function DefaultModal() { + const [openModal, setOpenModal] = useState(false); + + return ( + <> + + setOpenModal(false)}> + Terms of Service + +
    +

    + With less than a month to go before the European Union enacts new consumer privacy laws for its citizens, + companies around the world are updating their terms of service agreements to comply. +

    +

    + The European Union's General Data Protection Regulation (G.D.P.R.) goes into effect on May 25 and is meant to + ensure a common set of data rights in the European Union. +

    +
    +
    + + + + +
    + + ); +} +``` + +### Dropdown + +```jsx +import { Dropdown } from 'flowbite-react'; + +export default function DropdownExample() { + return ( + + Dashboard + Settings + Earnings + Sign out + + ); +} +``` + +### Navbar + +```jsx +import { Navbar } from 'flowbite-react'; + +export default function NavbarExample() { + return ( + + + Logo + + My App + + + + + + Home + + About + Services + Pricing + Contact + + + ); +} +``` + +### Card + +```jsx +import { Card } from 'flowbite-react'; + +export default function CardExample() { + return ( + +
    + Noteworthy technology acquisitions +
    +

    + Here are the biggest enterprise technology acquisitions of 2021 so far, + in reverse chronological order. +

    +
    + ); +} +``` + +### Form with validation + +```jsx +import { Button, Label, TextInput } from 'flowbite-react'; + +export default function FormExample() { + const handleSubmit = (e) => { + e.preventDefault(); + // Handle form submission + }; + + return ( +
    +
    +
    +
    + +
    +
    +
    +
    + +
    + +
    + ); +} +``` + +## Meteor.js starter project + +The Flowbite community has created an open-source Meteor.js starter project that has Tailwind CSS and Flowbite React set up beforehand. You can clone it by checking out the [repository on GitHub](https://github.com/meteor/flowbite-meteor-starter). + +## Further reading + +- [Flowbite React Components](https://flowbite-react.com/) +- [Flowbite Documentation](https://flowbite.com/docs/) +- [Tailwind CSS Documentation](https://tailwindcss.com/docs) +- [Flowbite React GitHub Repository](https://github.com/themesberg/flowbite-react) diff --git a/v3-docs/docs/tutorials/integrations/react-native.md b/v3-docs/docs/tutorials/integrations/react-native.md new file mode 100644 index 0000000000..89787cd68b --- /dev/null +++ b/v3-docs/docs/tutorials/integrations/react-native.md @@ -0,0 +1,220 @@ +# React Native Integration + +React Native has grown to be one of the most popular platforms for building native apps, being used by companies like Tesla, Instagram, and Facebook in production. React Native allows you to write apps in JavaScript that are rendered with native code. It has many of the features that you value when working with Meteor, like instant refresh on save. + +After reading this guide, you'll know: + +1. How to set up a React Native project with Meteor +2. How to connect React Native to your Meteor server +3. How to use Meteor's data layer in React Native +4. How to use Meteor Accounts in React Native + +## Overview + +You can easily integrate your React Native app with Meteor, using the same methods you would on a Meteor + React Web app. The integration supports most Meteor features, including Methods, Pub/Sub, and Password Accounts, and has the same usage as `react-meteor-data`. + +## Getting started with React Native + +React Native projects are coded using the same React principles, but have a completely separate codebase from your Meteor project. + +A collection of npm packages are being developed to make it easy for you to integrate React Native with Meteor. In order to use React Native with Meteor, you create a React Native app and use the `@meteorrn/core` package to connect your app to your Meteor server. + +For most projects, since your native app will display the same data and call the same methods as your Meteor web app, creating a React Native app that connects to your Meteor server does not require any changes to your Meteor codebase. + +The only time you will need to make changes to your Meteor codebase is to enable certain features that are unique to your native app. For example, if you want to add push notifications to your native app, you will need to create a method on your Meteor app to store the native push tokens for a user. + +### Expo vs. Vanilla React Native + +There are two routes for getting started with React Native. You can use "Vanilla" React Native, or you can use [Expo](https://expo.io/). Expo is a set of tools built around React Native. You can even try out React Native from your web browser using [Expo Snack](https://snack.expo.io/). + +**Downsides to using Expo:** + +- You cannot add Native Modules that use Native Code (Java, Swift, etc) +- You cannot use packages that require linking (npm modules that include native code) +- Apps that use Expo are much larger than pure React Native apps + +Expo does provide some native features ([click here for the full list](https://docs.expo.io/versions/latest/)), but if there is a feature missing that you need, you'll likely need to use an npm package or your own custom native code. + +You can "eject" your app from Expo to take advantage of Vanilla React Native features, but ejection cannot be undone easily. + +Here is the link to the React Native getting started documentation: https://reactnative.dev/docs/environment-setup + +Once you have your environment setup and have your app running on your device or in the emulator, you can proceed to the next step. + +## Installation + +To install the `@meteorrn/core` package, run the following command in your React Native project: + +```bash +npm install --save @meteorrn/core +``` + +You also need to confirm you have the package's peer dependencies installed: + +- Confirm you have `@react-native-community/netinfo` installed +- Confirm you have `@react-native-async-storage/async-storage@>=1.8.1` installed + +The `@meteorrn/core` package enables your React Native app to establish a DDP connection with your Meteor server so it can receive data from publications and call server methods. It also provides access to core Meteor client methods like `Accounts.createUser` and `Meteor.loginWithPassword`, and allows you to display data in your app with the `withTracker` method or hooks. + +## Setup + +First, import `Meteor`, `withTracker`, and `Mongo`: + +```js +import Meteor, { Mongo, withTracker } from '@meteorrn/core'; +``` + +Next, you need to connect to your Meteor server. This should typically be at the start of your App.jsx: + +```js +Meteor.connect('wss://myapp.meteor.com/websocket'); +``` + +For local development, you can connect to your local Meteor server: + +```js +// Use your computer's IP address (not localhost) for the mobile device to connect +Meteor.connect('ws://192.168.1.100:3000/websocket'); +``` + +Define your collections: + +```js +const Todos = new Mongo.Collection('todos'); +``` + +And now you're ready to start coding. + +## Coding with Meteor React Native + +If you've used React before, coding with React Native is pretty straightforward. However, instead of components like `div` and `span`, we have `View` and `Text`. You can learn the fundamentals of React Native [here](https://reactnative.dev/docs/intro-react). + +Meteor React Native's usage is designed to be as close to `meteor/react-meteor-data` and the Meteor core as possible. + +### Using withTracker + +```jsx +import React from 'react'; +import { View, Text, ScrollView } from 'react-native'; +import Meteor, { Mongo, withTracker } from '@meteorrn/core'; + +const Todos = new Mongo.Collection('todos'); + +function MyApp({ loading, myTodoTasks }) { + if (loading) { + return ( + + Loading your tasks... + + ); + } + + return ( + + {!myTodoTasks.length ? ( + You don't have any tasks + ) : ( + myTodoTasks.map(task => ( + {task.text} + )) + )} + + ); +} + +export default withTracker(() => { + const handle = Meteor.subscribe('myTodos'); + const myTodoTasks = Todos.find({ completed: false }).fetch(); + + return { + myTodoTasks, + loading: !handle.ready() + }; +})(MyApp); +``` + +### Using Accounts + +The package also has full support for accounts, including `Meteor.loginWithPassword`, `Meteor.user`, `Accounts.createUser`, `Meteor.loggingIn`, `Accounts.forgotPassword`, etc. + +```jsx +import React, { useState } from 'react'; +import { View, TextInput, Button, Text } from 'react-native'; +import Meteor from '@meteorrn/core'; + +function LoginScreen() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + + const handleLogin = () => { + Meteor.loginWithPassword(email, password, (err) => { + if (err) { + setError(err.reason); + } + }); + }; + + return ( + + + + {error && {error}} + + + ); +} +``` + +## Loading data with Methods + +Since Methods can work as general purpose RPCs, they can also be used to fetch data instead of publications. There are some advantages and some disadvantages to this approach compared with loading data through publications. + +Methods can be useful to fetch the result of a complex computation from the server that doesn't need to update when the server data changes. The biggest disadvantage of fetching data through Methods is that the data won't be automatically loaded into Minimongo, Meteor's client-side data cache, so you'll need to manage the lifecycle of that data manually. + +### Using a local collection to store Method data + +Collections are a convenient way of storing data on the client side. You can create a _local collection_ that exists only on the client: + +```js +// In client-side code, declare a local collection +const ScoreAverages = new Mongo.Collection(null); +``` + +Now, if you fetch data using a Method, you can put it into this collection: + +```js +import { calculateAverages } from '/imports/api/games/methods'; + +async function updateAverages() { + // Clean out result cache + await ScoreAverages.removeAsync({}); + + // Call a Method that does an expensive computation + const results = await calculateAverages(); + + for (const item of results) { + await ScoreAverages.insertAsync(item); + } +} +``` + +You can now use the data from the local collection `ScoreAverages` inside a UI component exactly the same way you would use a regular MongoDB collection. + +## Advanced concepts + +### Method call lifecycle + +Here's exactly what happens, in order, when a Method is called: + +#### 1. Method simulation runs on the client + +If we defined this Method in client and server code, as all Methods should be, a Method simulation is executed in the client that called it. + +The client enters a special mode where it tracks all changes made to client-side collections, so that they can be rolled back later. When this step is complete, the user sees their UI update instantly with the new content of the client-side database, but the server hasn't received any data yet. + +#### 2. A `method` DDP message is sent to the server + +The Meteor client constructs a DDP message to send to the server. This includes the Method name, arguments, and an automatically generated Method ID. + +#### 3. Method runs on the server + +When the server receives the message, it executes the Method code again on the server. The client side version was a simulation that will be rolled back later, but this time it's the real version that is writing to the actual database. + +#### 4. Return value is sent to the client + +Once the Method has finished running on the server, it sends a `result` message to the client with the Method ID and the return value. + +#### 5. Any DDP publications affected by the Method are updated + +If we have any publications on the page that have been affected by the database writes from this Method, the server sends the appropriate updates to the client. + +#### 6. `updated` message sent, data replaced, callback fires + +After the relevant data updates have been sent to the client, the server sends back the `updated` message. The client rolls back any changes from the Method simulation and replaces them with the actual changes sent from the server. + +Lastly, the Method promise resolves with the return value. It's important that this waits until the client is up to date, so that your Method callback can assume that the client state reflects any changes done inside the Method. + +### Benefits of Methods over REST + +Methods provide many benefits over REST endpoints: + +#### Methods use async/await, but are non-blocking + +You can write code that uses return values and throws errors using async/await syntax, and avoid dealing with lots of nested callbacks. + +#### Methods always run and return in order + +When multiple Method calls are received from the same client, Meteor runs each Method to completion before starting the next one. If you need to disable this functionality for one particularly long-running Method, you can use `this.unblock()` to allow the next Method to run while the current one is still in progress. + +#### Change tracking for Optimistic UI + +When Method simulations and server-side executions run, Meteor tracks any resulting changes to the database. This is what lets the Meteor data system roll back the changes from the Method simulation and replace them with the actual writes from the server. + +### Calling a Method from another Method + +Sometimes, you'll want to call a Method from another Method. This is a totally fine pattern: + +1. Inside a client-side Method simulation, calling another Method doesn't fire off an extra request to the server—it runs the simulation of the called Method. +2. Inside a Method execution on the server, calling another Method runs that Method as if it were called by the same client, with the same context (`userId`, `connection`, etc). + +### Consistent ID generation and Optimistic UI + +When you insert documents into Minimongo from the client-side simulation of a Method, the `_id` field of each document is a random string. Each Meteor Method invocation shares a random generator seed with the client that called the Method, so any IDs generated by the client and server Methods are guaranteed to be the same. + +This means you can safely use the IDs generated on the client to do things while the Method is being sent to the server. For example, you can create a new document and immediately redirect to a URL that contains that document's ID. + +### Method retries + +If you call a Method from the client, and the user's Internet connection disconnects before the result is received, Meteor assumes that the Method didn't actually run. When the connection is re-established, the Method call will be sent again. + +This means that, in certain situations, Methods can be sent more than once. For this reason, you should try to make Methods idempotent—calling them multiple times doesn't result in additional changes to the database. + +Many Method operations are idempotent by default: +- Inserts will throw an error if they happen twice because the generated ID will conflict +- Removes on collections won't do anything the second time +- Most update operators like `$set` will have the same result if run again + +The places you need to be careful are MongoDB update operators that stack, like `$inc` and `$push`, and calls to external APIs. diff --git a/v3-docs/docs/tutorials/react/2.collections.md b/v3-docs/docs/tutorials/react/2.collections.md index a461e6da41..2d509b9071 100644 --- a/v3-docs/docs/tutorials/react/2.collections.md +++ b/v3-docs/docs/tutorials/react/2.collections.md @@ -2,7 +2,7 @@ Meteor already sets up MongoDB for you. In order to use our database, we need to create a _collection_, which is where we will store our _documents_, in our case our `tasks`. -> You can read more about collections [here](https://v3-docs.meteor.com/api/collections.html). +> You can read more about collections [here](/api/collections). In this step, we will implement all the necessary code to have a basic collection for our tasks up and running using React hooks. @@ -24,7 +24,7 @@ Notice that we stored the file in the `imports/api` directory, which is a place You can delete the `links.js` file in this folder as we are not going to use this collection. -> You can read more about app structure and imports/exports [here](http://guide.meteor.com/structure.html). +> You can read more about app structure and imports/exports [here](/tutorials/application-structure/). ### 2.2: Initialize Tasks Collection {#initialize-tasks-collection} @@ -166,7 +166,7 @@ export const App = () => { As you can see, when subscribing to a publication using `useSubscribe` you'll get a `isLoading` function, that you can use to render some loading component before the data is ready. -> For more information on Publications/Subscriptions, please check our [docs](https://v3-docs.meteor.com/api/meteor.html#pubsub). +> For more information on Publications/Subscriptions, please check our [docs](/api/meteor#pubsub). See how your app should look like now: diff --git a/v3-docs/docs/tutorials/react/3.forms-and-events.md b/v3-docs/docs/tutorials/react/3.forms-and-events.md index 01ab1096f8..36a6b253bd 100644 --- a/v3-docs/docs/tutorials/react/3.forms-and-events.md +++ b/v3-docs/docs/tutorials/react/3.forms-and-events.md @@ -88,7 +88,7 @@ You also can style it as you wish. For now, we only need some margin at the top Now let's create a function to handle the form submit and insert a new task into the database. To do it, we will need to implement a Meteor Method. -Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](https://guide.meteor.com/methods.html). +Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](/tutorials/methods/methods). To create your methods, you can create a file called `tasksMethods.js`. @@ -179,7 +179,7 @@ Also, insert a date `createdAt` in your `task` document so you know when each ta ### 3.5: Show Newest Tasks First -Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](https://guide.meteor.com/collections.html#mongo-collections) query. +Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](/tutorials/collections/collections#mongo-collections) query. ::: code-group diff --git a/v3-docs/docs/tutorials/react/7.adding-user-accounts.md b/v3-docs/docs/tutorials/react/7.adding-user-accounts.md index 6b1513afbb..9b37e3a66a 100644 --- a/v3-docs/docs/tutorials/react/7.adding-user-accounts.md +++ b/v3-docs/docs/tutorials/react/7.adding-user-accounts.md @@ -8,7 +8,7 @@ Meteor already comes with a basic authentication and account management system o meteor add accounts-password ``` -> There are many more authentication methods supported. You can read more about the accounts system [here](https://v3-docs.meteor.com/api/accounts.html). +> There are many more authentication methods supported. You can read more about the accounts system [here](/api/accounts). We also recommend you to install `bcrypt` node module, otherwise, you are going to see a warning saying that you are using a pure-Javascript implementation of it. diff --git a/v3-docs/docs/tutorials/react/8.deploying.md b/v3-docs/docs/tutorials/react/8.deploying.md index 11f3e85f0e..0000c77f73 100644 --- a/v3-docs/docs/tutorials/react/8.deploying.md +++ b/v3-docs/docs/tutorials/react/8.deploying.md @@ -1,6 +1,6 @@ ## 8: Deploying -Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](https://guide.meteor.com/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. +Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](/tutorials/deployment/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. In this tutorial, we will deploy our app on [Galaxy](https://www.meteor.com/hosting), which is our own cloud solution. Galaxy offers a free plan, so you can deploy and test your app. Pretty cool, right? diff --git a/v3-docs/docs/tutorials/routing/routing.md b/v3-docs/docs/tutorials/routing/routing.md new file mode 100644 index 0000000000..2f66c8e00f --- /dev/null +++ b/v3-docs/docs/tutorials/routing/routing.md @@ -0,0 +1,505 @@ +# URLs and Routing + +After reading this guide, you'll know: + +1. The role URLs play in a client-rendered app, and how it's different from a traditional server-rendered app. +2. How to define client and server routes for your app using Flow Router. +3. How to have your app display different content depending on the URL. +4. How to dynamically load application modules depending on the URL. +5. How to construct links to routes and go to routes programmatically. + +## Client-side Routing + +In a web application, _routing_ is the process of using URLs to drive the user interface (UI). URLs are a prominent feature in every single web browser, and have several main functions from the user's point of view: + +1. **Bookmarking** - Users can bookmark URLs in their web browser to save content they want to come back to later. +2. **Sharing** - Users can share content with others by sending a link to a certain page. +3. **Navigation** - URLs are used to drive the web browser's back/forward functions. + +In a traditional web application stack, where the server renders HTML one page at a time, the URL is the fundamental entry point for the user to access the application. Users navigate an application by clicking through URLs, which are sent to the server via HTTP, and the server responds appropriately via a server-side router. + +In contrast, Meteor operates on the principle of _data on the wire_, where the server doesn't think in terms of URLs or HTML pages. The client application communicates with the server over DDP. Typically as an application loads, it initializes a series of _subscriptions_ which fetch the data required to render the application. As the user interacts with the application, different subscriptions may load, but there's no technical need for URLs to be involved in this process - you could have a Meteor app where the URL never changes. + +However, most of the user-facing features of URLs listed above are still relevant for typical Meteor applications. Since the server is not URL-driven, the URL becomes a useful representation of the client-side state the user is currently looking at. However, unlike in a server-rendered application, it does not need to describe the entirety of the user's current state; it needs to contain the parts that you want to be linkable. For example, the URL should contain any search filters applied on a page, but not necessarily the state of a dropdown menu or popup. + +## Using Flow Router + +To add routing to your app, install the [`ostrio:flow-router-extra`](https://atmospherejs.com/ostrio/flow-router-extra) package: + +```bash +meteor add ostrio:flow-router-extra +``` + +Flow Router Extra is the community routing package for Meteor. It is a carefully extended `flow-router` package with additional features like `waitOn`, template context, and built-in `.render()`. The packages `arillo:flow-router-helpers` and `zimme:active-route` are already built into Flow Router Extra and updated to support the latest Meteor release. + +## Defining a simple route + +The basic purpose of a router is to match certain URLs and perform actions as a result. This all happens on the client side, in the app user's browser or mobile app container. Let's take an example from the Todos example app: + +```js +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; + +FlowRouter.route('/lists/:_id', { + name: 'Lists.show', + action(params, queryParams) { + console.log("Looking at a list?"); + } +}); +``` + +This route handler will run in two situations: if the page loads initially at a URL that matches the URL pattern, or if the URL changes to one that matches the pattern while the page is open. Note that, unlike in a server-side-rendered app, the URL can change without any additional requests to the server. + +When the route is matched, the `action` method executes, and you can perform any actions you need to. The `name` property of the route is optional, but will let us refer to this route more conveniently later on. + +### URL pattern matching + +Consider the following URL pattern, used in the code snippet above: + +```js +'/lists/:_id' +``` + +The above pattern will match certain URLs. You may notice that one segment of the URL is prefixed by `:` - this means that it is a *url parameter*, and will match any string that is present in that segment of the path. Flow Router will make that part of the URL available on the `params` property of the current route. + +Additionally, the URL could contain an HTTP [*query string*](https://en.wikipedia.org/wiki/Query_string) (the part after an optional `?`). If so, Flow Router will also split it up into named parameters, which it calls `queryParams`. + +Here are some example URLs and the resulting `params` and `queryParams`: + +| URL | matches pattern? | params | queryParams | +| ---- | ---- | ---- | ---- | +| / | no | | | +| /about | no | | | +| /lists/ | no | | | +| /lists/eMtGij5AFESbTKfkT | yes | { _id: "eMtGij5AFESbTKfkT"} | { } | +| /lists/1 | yes | { _id: "1"} | { } | +| /lists/1?todoSort=top | yes | { _id: "1"} | { todoSort: "top" } | + +Note that all of the values in `params` and `queryParams` are always strings since URLs don't have any way of encoding data types. For example, if you wanted a parameter to represent a number, you might need to use `parseInt(value, 10)` to convert it when you access it. + +## Accessing Route information + +In addition to passing in the parameters as arguments to the `action` function on the route, Flow Router makes a variety of information available via (reactive and otherwise) functions on the global singleton `FlowRouter`. As the user navigates around your app, the values of these functions will change (reactively in some cases) correspondingly. + +Like any other global singleton in your application, it's best to limit your access to `FlowRouter`. That way the parts of your app will remain modular and more independent. In the case of `FlowRouter`, it's best to access it solely from the top of your component hierarchy, either in the "page" component, or the layouts that wrap it. + +### The current route + +It's useful to access information about the current route in your code. Here are some reactive functions you can call: + +* `FlowRouter.getRouteName()` gets the name of the route +* `FlowRouter.getParam(paramName)` returns the value of a single URL parameter +* `FlowRouter.getQueryParam(paramName)` returns the value of a single URL query parameter + +In our example of the list page from the Todos app, we access the current list's id with `FlowRouter.getParam('_id')`. + +### Highlighting the active route + +One situation where it is sensible to access the global `FlowRouter` singleton to access the current route's information deeper in the component hierarchy is when rendering links via a navigation component. It's often required to highlight the "active" route in some way (this is the route or section of the site that the user is currently looking at). + +In the Todos example app, we link to each list the user knows about in the `App_body` template: + +```html +{{#each list in lists}} + + ... + {{list.name}} + +{{/each}} +``` + +We can determine if the user is currently viewing the list with the `activeListClass` helper: + +```js +Template.App_body.helpers({ + activeListClass(list) { + const active = ActiveRoute.name('Lists.show') + && FlowRouter.getParam('_id') === list._id; + + return active && 'active'; + } +}); +``` + +## Rendering based on the route + +Now we understand how to define routes and access information about the current route, we are in a position to do what you usually want to do when a user accesses a route---render a user interface to the screen that represents it. + +### Rendering with Blaze + +When using Flow Router with Blaze, the simplest way to display different views on the page for different URLs is to use the complementary Blaze Layout package. First, make sure you have the Blaze Layout package installed: + +```bash +meteor add kadira:blaze-layout +``` + +To use this package, we need to define a "layout" component. In the Todos example app, that component is called `App_body`: + +```html + +``` + +Here, we are using a Blaze feature called `Template.dynamic` to render a template which is attached to the `main` property of the data context. Using Blaze Layout, we can change that `main` property when a route is accessed. + +We do that in the `action` function of our `Lists.show` route definition: + +```js +import { BlazeLayout } from 'meteor/kadira:blaze-layout'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; + +FlowRouter.route('/lists/:_id', { + name: 'Lists.show', + action() { + BlazeLayout.render('App_body', { main: 'Lists_show_page' }); + } +}); +``` + +What this means is that whenever a user visits a URL of the form `/lists/X`, the `Lists.show` route will kick in, triggering the `BlazeLayout` call to set the `main` property of the `App_body` component. + +### Rendering with React + +When using React, you can render components directly in the route action. Here's how to set up React with Flow Router: + +```js +import React from 'react'; +import { createRoot } from 'react-dom/client'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; + +// Create a root element for React +const rootElement = document.getElementById('react-root'); +const root = createRoot(rootElement); + +FlowRouter.route('/lists/:_id', { + name: 'Lists.show', + action(params) { + root.render(); + } +}); +``` + +Or you can use `react-mount` package for a more integrated approach: + +```bash +meteor npm install react-mounter +``` + +```js +import { mount } from 'react-mounter'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import { MainLayout } from './layouts/MainLayout'; +import { ListsShowPage } from './pages/ListsShowPage'; + +FlowRouter.route('/lists/:_id', { + name: 'Lists.show', + action(params) { + mount(MainLayout, { + content: + }); + } +}); +``` + +## Components as pages + +Notice that we called the component to be rendered `Lists_show_page` (rather than `Lists_show`). This indicates that this template is rendered directly by a Flow Router action and forms the 'top' of the rendering hierarchy for this URL. + +The `Lists_show_page` template renders *without* arguments---it is this template's responsibility to collect information from the current route, and then pass this information down into its child templates. Correspondingly the `Lists_show_page` template is very tied to the route that rendered it, and so it needs to be a smart component. + +It makes sense for a "page" smart component like `Lists_show_page` to: + +1. Collect route information, +2. Subscribe to relevant subscriptions, +3. Fetch the data from those subscriptions, and +4. Pass that data into a sub-component. + +In this case, the HTML template for `Lists_show_page` will look very simple, with most of the logic in the JavaScript code: + +```html + +``` + +```js +Template.Lists_show_page.helpers({ + listIdArray() { + const instance = Template.instance(); + const listId = instance.getListId(); + return Lists.findOne(listId) ? [listId] : []; + }, + async listArgs(listId) { + const instance = Template.instance(); + return { + todosReady: instance.subscriptionsReady(), + list() { + return Lists.findOne(listId); + }, + todos: await Lists.findOneAsync(listId, { fields: { _id: true } }).todos() + }; + } +}); +``` + +### Changing page when logged out + +There are types of rendering logic that appear related to the route but which also seem related to user interface rendering. A classic example is authorization; for instance, you may want to render a login form for some subset of your pages if the user is not yet logged in. + +It's best to keep all logic around what to render in the component hierarchy. So this authorization should happen inside a component: + +```html + +``` + +You can create wrapper components using Blaze's "template as block helper" ability: + +```html + +``` + +Once that template exists, we can wrap our `Lists_show_page`: + +```html + +``` + +## Changing Routes + +Rendering an updated UI when a user reaches a new route is not that useful without giving the user some way to reach a new route! The simplest way is with the trusty `` tag and a URL. You can generate the URLs yourself using helpers such as `FlowRouter.pathFor` to display a link to a certain route: + +```html + +``` + +### Routing programmatically + +In some cases you want to change routes based on user action outside of them clicking on a link. For instance, in the example app, when a user creates a new list, we want to route them to the list they just created. We do this by calling `FlowRouter.go()` once we know the id of the new list: + +```js +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; + +Template.App_body.events({ + async 'click .js-new-list'() { + const listId = await insert.callAsync(); + FlowRouter.go('Lists.show', { _id: listId }); + } +}); +``` + +You can also change only part of the URL if you want to, using the `FlowRouter.setParams()` and `FlowRouter.setQueryParams()`. For instance, if we were viewing one list and wanted to go to another: + +```js +FlowRouter.setParams({ _id: newList._id }); +``` + +Of course, calling `FlowRouter.go()` will always work, so unless you are trying to optimize for a specific situation it's better to use that. + +### Storing data in the URL + +As we discussed in the introduction, the URL is really a serialization of some part of the client-side state the user is looking at. Although parameters can only be strings, it's possible to convert any type of data to a string by serializing it. + +In general if you want to store arbitrary serializable data in a URL param, you can use `EJSON.stringify()` to turn it into a string. You'll need to URL-encode the string using `encodeURIComponent` to remove any characters that have meaning in a URL: + +```js +import { EJSON } from 'meteor/ejson'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; + +FlowRouter.setQueryParams({ data: encodeURIComponent(EJSON.stringify(data)) }); +``` + +You can then get the data back out of Flow Router using `EJSON.parse()`. Note that Flow Router does the URL decoding for you automatically: + +```js +const data = EJSON.parse(FlowRouter.getQueryParam('data')); +``` + +## Redirecting + +Sometimes, your users will end up on a page that isn't a good place for them to be. Maybe the data they were looking for has moved, maybe they were on an admin panel page and logged out, or maybe they just created a new object and you want them to end up on the page for the thing they just created. + +Usually, we can redirect in response to a user's action by calling `FlowRouter.go()` and friends, like in our list creation example above, but if a user browses directly to a URL that doesn't exist, it's useful to know how to redirect immediately. + +If a URL is out-of-date (sometimes you might change the URL scheme of an application), you can redirect inside the `action` function of the route: + +```js +FlowRouter.route('/old-list-route/:_id', { + action(params) { + FlowRouter.go('Lists.show', params); + } +}); +``` + +### Redirecting dynamically + +The above approach will only work for static redirects. However, sometimes you need to load some data to figure out where to redirect to. In this case you'll need to render part of the component hierarchy to subscribe to the data you need. For example, in the Todos example app, we want to make the root (`/`) route redirect to the first known list: + +```js +FlowRouter.route('/', { + name: 'App.home', + action() { + BlazeLayout.render('App_body', { main: 'App_rootRedirector' }); + } +}); +``` + +The `App_rootRedirector` component is rendered inside the `App_body` layout: + +```js +Template.App_rootRedirector.onCreated(function rootRedirectorOnCreated() { + this.autorun(async () => { + if (this.subscriptionsReady()) { + const list = await Lists.findOneAsync(); + if (list) { + FlowRouter.go('Lists.show', { _id: list._id }); + } + } + }); +}); +``` + +### Redirecting after a user's action + +Often, you just want to go to a new route programmatically when a user has completed a certain action. If you want to wait for the method to return from the server, you can use async/await: + +```js +Template.App_body.events({ + async 'click .js-new-list'() { + try { + const listId = await lists.insert.callAsync(); + FlowRouter.go('Lists.show', { _id: listId }); + } catch (err) { + // Handle error - show message to user + console.error('Failed to create list:', err); + } + } +}); +``` + +You will also want to show some kind of indication that the method is working in between their click of the button and the redirect completing. Don't forget to provide feedback if the method is returning an error. + +## Advanced Routing + +### Dynamically load modules + +[Dynamic imports](https://docs.meteor.com/packages/dynamic-import) allow you to dramatically reduce the client's bundle size, and load modules and dependencies dynamically upon request, based on the current URI. + +Assume we have `index.html` and `index.js` with code for `index` template and this is the only place in the application where it depends on the large `moment` package. This means the `moment` package is not needed in the other parts of our app, and it will only waste bandwidth and slow load time. + +```html + + +``` + +```js +// /imports/client/index.js +import moment from 'moment'; +import { Template } from 'meteor/templating'; +import './index.html'; + +Template.index.helpers({ + time() { + return moment().format('LTS'); + } +}); +``` + +```js +// /imports/lib/routes.js +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import { BlazeLayout } from 'meteor/kadira:blaze-layout'; + +FlowRouter.route('/', { + name: 'index', + waitOn() { + // Wait for index.js to load over the wire + return import('/imports/client/index.js'); + }, + action() { + BlazeLayout.render('App_body', { main: 'index' }); + } +}); +``` + +### Missing pages (404) + +If a user types an incorrect URL, chances are you want to show them some kind of amusing not-found page. There are actually two categories of not-found pages. The first is when the URL typed in doesn't match any of your route definitions. You can use `FlowRouter.notFound` to handle this: + +```js +FlowRouter.notFound = { + action() { + BlazeLayout.render('App_body', { main: 'App_notFound' }); + } +}; +``` + +The second is when the URL is valid, but doesn't actually match any data. In this case, the URL matches a route, but once the route has successfully subscribed, it discovers there is no data. It usually makes sense in this case for the page component to render a not-found template instead of the usual template for the page: + +```html + +``` + +### Analytics + +It's common to want to know which pages of your app are most commonly visited, and where users are coming from. You can read about how to set up Flow Router based analytics in the [Deployment Guide](/tutorials/deployment/deployment#analytics). + +### Server Side Routing + +As we've discussed, Meteor is a framework for client rendered applications, but this doesn't always remove the requirement for server rendered routes. There are three main use cases for server-side routing. + +#### Server Routing for API access + +Although Meteor allows you to write low-level connect handlers to create any kind of API you like on the server-side, if all you want to do is create a RESTful version of your Methods and Publications, you can often use the [`simple:rest`](http://atmospherejs.com/simple/rest) package. + +If you need more control, you can use the comprehensive [`nimble:restivus`](https://atmospherejs.com/nimble/restivus) package to create more or less whatever you need in whatever ontology you require. + +#### Server Rendering + +While Blaze does not have support for server-side rendering, React does. This means it is possible to render HTML on the server if you use React as your rendering framework. + +For server-side rendering with React, consider using packages like [`server-render`](/packages/server-render) which provides utilities for SSR in Meteor. + +#### Server Routing for additional resources + +There might be additional resources that you want to make available on your server or receive web hooks. If you need anything more complicated with dynamic parts of the URL you might want to implement [Picker](https://atmospherejs.com/communitypackages/picker) which is a simple server-side router that handles dynamic routes. + +If you need to authenticate the user when providing additional server-side resources such as PDF documents or XLSX spreadsheets, you can use [`mhagmajer:server-router`](https://atmospherejs.com/mhagmajer/server-router) package to do this easily. diff --git a/v3-docs/docs/tutorials/security/security.md b/v3-docs/docs/tutorials/security/security.md index 4901019c41..73fcb06202 100644 --- a/v3-docs/docs/tutorials/security/security.md +++ b/v3-docs/docs/tutorials/security/security.md @@ -80,7 +80,7 @@ If someone comes along and passes a non-ID selector like `{}`, they will end up ### jam:method -To help you write good Methods that exhaustively validate their arguments, you can use a community package for Methods that enforces argument validation. Read more about how to use it in the [documentation for jam:method](/community-packages/jam-method.html). The rest of the code samples in this article will assume that you are using this package. If you aren't, you can still apply the same principles but the code will look a little different. +To help you write good Methods that exhaustively validate their arguments, you can use a community package for Methods that enforces argument validation. Read more about how to use it in the [documentation for jam:method](/community-packages/jam-method). The rest of the code samples in this article will assume that you are using this package. If you aren't, you can still apply the same principles but the code will look a little different. ### Never pass userId from the client @@ -311,7 +311,7 @@ Meteor.publish('list', function (listId) { In the first example, if the `userId` property on the selected list changes, the query in the publication will still return the data, since the security check in the beginning will not re-run. In the second example, we have fixed this by putting the security check in the returned query itself. -Unfortunately, not all publications are as simple to secure as the example above. For more tips on how to use `reywood:publish-composite` to handle reactive changes in publications, see the [data loading article](https://guide.meteor.com/data-loading#complex-auth). +Unfortunately, not all publications are as simple to secure as the example above. For more tips on how to use `reywood:publish-composite` to handle reactive changes in publications, see the [data loading article](/tutorials/data-loading/data-loading#complex-auth). ### Passing options @@ -402,7 +402,7 @@ Here's what a settings file with some API keys might look like: In your app's JavaScript code, these settings can be accessed from the variable `Meteor.settings`. -[Read more about managing keys and settings in the Deployment article.](https://guide.meteor.com/deployment) +[Read more about managing keys and settings in the Deployment article.](/tutorials/deployment/deployment) ### Settings on the client @@ -461,8 +461,8 @@ Yes, Meteor does hash your password or login token on the client before sending #### Setting up SSL -* On [Galaxy](https://guide.meteor.com/deployment#galaxy), configuration of SSL is automatic. [See the help article about SSL on Galaxy](https://help.galaxycloud.app/en/article/encryption-pt8wbl/). -* If you are running on your own [infrastructure](https://guide.meteor.com/deployment#custom-deployment), there are a few options for setting up SSL, mostly through configuring a proxy web server. See the articles: [Josh Owens on SSL and Meteor](http://joshowens.me/ssl-and-meteor-js/), [SSL on Meteorpedia](http://www.meteorpedia.com/read/SSL), and [Digital Ocean tutorial with an Nginx config](https://www.digitalocean.com/community/tutorials/how-to-deploy-a-meteor-js-application-on-ubuntu-14-04-with-nginx). +* On [Galaxy](/tutorials/deployment/deployment#galaxy-cloud-recommended), configuration of SSL is automatic. [See the help article about SSL on Galaxy](https://help.galaxycloud.app/en/article/encryption-pt8wbl/). +* If you are running on your own [infrastructure](/tutorials/deployment/deployment#custom-deployment), there are a few options for setting up SSL, mostly through configuring a proxy web server. See the articles: [Josh Owens on SSL and Meteor](http://joshowens.me/ssl-and-meteor-js/), [SSL on Meteorpedia](http://www.meteorpedia.com/read/SSL), and [Digital Ocean tutorial with an Nginx config](https://www.digitalocean.com/community/tutorials/how-to-deploy-a-meteor-js-application-on-ubuntu-14-04-with-nginx). #### Forcing SSL @@ -470,7 +470,7 @@ Generally speaking, all production HTTP requests should go over HTTPS, and all W It's best to handle the redirection from HTTP to HTTPS on the platform which handles the SSL certificates and termination. -* On [Galaxy](https://guide.meteor.com/deployment#galaxy), enable the "Force HTTPS" setting on a specific domain in the "Domains & Encryption" section of the application's "Settings" tab. +* On [Galaxy](/tutorials/deployment/deployment#galaxy-cloud-recommended), enable the "Force HTTPS" setting on a specific domain in the "Domains & Encryption" section of the application's "Settings" tab. * Other deployments *may* have control panel options or may need to be manually configured on the proxy server (e.g. HAProxy, nginx, etc.). The articles linked above provide some assistance on this. In the event that a platform does not offer the ability to configure this, the `force-ssl` package can be added to the project and Meteor will attempt to intelligently redirect based on the presence of the `x-forwarded-for` header. @@ -708,7 +708,7 @@ This is a collection of points to check about your app that might catch common e 1. Make sure your app doesn't have the `insecure` or `autopublish` packages. 1. Validate all Method and publication arguments, and include the `audit-argument-checks` to check this automatically. 1. Apply rate limiting to your application to prevent DDoS attacks. -1. [Deny writes to the `profile` field on user documents.](https://guide.meteor.com/accounts#dont-use-profile) +1. [Deny writes to the `profile` field on user documents.](/tutorials/accounts/accounts#dont-use-profile) 1. [Use Methods instead of client-side insert/update/remove and allow/deny.](#avoid-allow-deny) 1. Use specific selectors and [filter fields](#always-restrict-fields) in publications. 1. Don't use [raw HTML inclusion in Blaze](http://blazejs.org/guide/spacebars.html#Rendering-raw-HTML) unless you really know what you are doing. diff --git a/v3-docs/docs/tutorials/solid/2.collections.md b/v3-docs/docs/tutorials/solid/2.collections.md index 8e7c6b1e3b..a5bd75d10c 100644 --- a/v3-docs/docs/tutorials/solid/2.collections.md +++ b/v3-docs/docs/tutorials/solid/2.collections.md @@ -2,7 +2,7 @@ Meteor already sets up MongoDB for you. In order to use our database, we need to create a _collection_, which is where we will store our _documents_, in our case our `tasks`. -> You can read more about collections [here](https://v3-docs.meteor.com/api/collections.html). +> You can read more about collections [here](/api/collections). In this step we will implement all the necessary code to have a basic collection for our tasks up and running. @@ -23,7 +23,7 @@ Notice that we stored the file in the `imports/api` directory, which is a place If there is a `imports/api/links.js` file from the starter project, you can delete that now as we don't need it. -> You can read more about app structure and imports/exports [here](http://guide.meteor.com/structure.html). +> You can read more about app structure and imports/exports [here](/tutorials/application-structure/). ### 2.2: Initialize Tasks Collection @@ -114,7 +114,7 @@ But wait! Something is missing. If you run your app now, you'll see that you don That's because we need to publish our data to the client. -> For more information on Publications/Subscriptions, please check our [docs](https://v3-docs.meteor.com/api/meteor.html#pubsub). +> For more information on Publications/Subscriptions, please check our [docs](/api/meteor#pubsub). Meteor doesn't need REST calls. It instead relies on synchronizing the MongoDB on the server with a MiniMongoDB on the client. It does this by first publishing collections on the server and then subscribing to them on the client. diff --git a/v3-docs/docs/tutorials/solid/3.forms-and-events.md b/v3-docs/docs/tutorials/solid/3.forms-and-events.md index c6aef8a73e..ea964be842 100644 --- a/v3-docs/docs/tutorials/solid/3.forms-and-events.md +++ b/v3-docs/docs/tutorials/solid/3.forms-and-events.md @@ -146,7 +146,7 @@ You also can style it as you wish. For now, we only need some margin at the top Now let's create a function to handle the form submit and insert a new task into the database. To do it, we will need to implement a Meteor Method. -Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](https://guide.meteor.com/methods.html). +Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](/tutorials/methods/methods). To create your methods, you can create a file called `TasksMethods.js`. @@ -223,7 +223,7 @@ Meteor methods execute optimistically on the client using MiniMongo while simult ### 3.4: Show Newest Tasks First -Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](https://guide.meteor.com/collections.html#mongo-collections) query. +Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](/tutorials/collections/collections#mongo-collections) query. ::: code-group diff --git a/v3-docs/docs/tutorials/solid/7.adding-user-accounts.md b/v3-docs/docs/tutorials/solid/7.adding-user-accounts.md index 23d78a2682..690f77b65a 100644 --- a/v3-docs/docs/tutorials/solid/7.adding-user-accounts.md +++ b/v3-docs/docs/tutorials/solid/7.adding-user-accounts.md @@ -8,7 +8,7 @@ Meteor already comes with a basic authentication and account management system o meteor add accounts-password ``` -> There are many more authentication methods supported. You can read more about the accounts system [here](https://v3-docs.meteor.com/api/accounts.html). +> There are many more authentication methods supported. You can read more about the accounts system [here](/api/accounts). We also recommend you to install `bcrypt` node module, otherwise, you are going to see a warning saying that you are using a pure-Javascript implementation of it. diff --git a/v3-docs/docs/tutorials/solid/8.deploying.md b/v3-docs/docs/tutorials/solid/8.deploying.md index 589ab8d062..0731ae28f9 100644 --- a/v3-docs/docs/tutorials/solid/8.deploying.md +++ b/v3-docs/docs/tutorials/solid/8.deploying.md @@ -1,6 +1,6 @@ ## 8: Deploying -Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](https://guide.meteor.com/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. +Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](/tutorials/deployment/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. In this tutorial, we will deploy our app on [Galaxy](https://galaxycloud.app/), which is our own cloud solution. Galaxy offers a free plan, so you can deploy and test your app. Pretty cool, right? diff --git a/v3-docs/docs/tutorials/svelte/2.collections.md b/v3-docs/docs/tutorials/svelte/2.collections.md index 9e92419acc..ebafc04f5a 100644 --- a/v3-docs/docs/tutorials/svelte/2.collections.md +++ b/v3-docs/docs/tutorials/svelte/2.collections.md @@ -2,7 +2,7 @@ Meteor already sets up MongoDB for you. In order to use our database, we need to create a _collection_, which is where we will store our _documents_, in our case our `tasks`. -> You can read more about collections [here](https://v3-docs.meteor.com/api/collections.html). +> You can read more about collections [here](/api/collections). In this step we will implement all the necessary code to have a basic collection for our tasks up and running. @@ -23,7 +23,7 @@ Notice that we stored the file in the `imports/api` directory, which is a place If there is a `imports/api/links.js` file from the starter project, you can delete that now as we don't need it. -> You can read more about app structure and imports/exports [here](http://guide.meteor.com/structure.html). +> You can read more about app structure and imports/exports [here](/tutorials/application-structure/). ### 2.2: Initialize Tasks Collection @@ -126,7 +126,7 @@ But wait! Something is missing. If you run your app now, you'll see that you don That's because we need to publish our data to the client. -> For more information on Publications/Subscriptions, please check our [docs](https://v3-docs.meteor.com/api/meteor.html#pubsub). +> For more information on Publications/Subscriptions, please check our [docs](/api/meteor#pubsub). Meteor doesn't need REST calls. It instead relies on synchronizing the MongoDB on the server with a MiniMongoDB on the client. It does this by first publishing collections on the server and then subscribing to them on the client. diff --git a/v3-docs/docs/tutorials/svelte/3.forms-and-events.md b/v3-docs/docs/tutorials/svelte/3.forms-and-events.md index 2e745059ef..7ecf20b582 100644 --- a/v3-docs/docs/tutorials/svelte/3.forms-and-events.md +++ b/v3-docs/docs/tutorials/svelte/3.forms-and-events.md @@ -145,7 +145,7 @@ You also can style it as you wish. For now, we only need some margin at the top Now let's create a function to handle the form submit and insert a new task into the database. To do it, we will need to implement a Meteor Method. -Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](https://guide.meteor.com/methods.html). +Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](/tutorials/methods/methods). To create your methods, you can create a file called `TasksMethods.js`. @@ -221,7 +221,7 @@ Meteor methods execute optimistically on the client using MiniMongo while simult ### 3.4: Show Newest Tasks First -Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](https://guide.meteor.com/collections.html#mongo-collections) query. +Now you just need to make a change that will make users happy: we need to show the newest tasks first. We can accomplish this quite quickly by sorting our [Mongo](/tutorials/collections/collections#mongo-collections) query. ::: code-group diff --git a/v3-docs/docs/tutorials/svelte/7.adding-user-accounts.md b/v3-docs/docs/tutorials/svelte/7.adding-user-accounts.md index 30d1902382..57ea342593 100644 --- a/v3-docs/docs/tutorials/svelte/7.adding-user-accounts.md +++ b/v3-docs/docs/tutorials/svelte/7.adding-user-accounts.md @@ -8,7 +8,7 @@ Meteor already comes with a basic authentication and account management system o meteor add accounts-password ``` -> There are many more authentication methods supported. You can read more about the accounts system [here](https://v3-docs.meteor.com/api/accounts.html). +> There are many more authentication methods supported. You can read more about the accounts system [here](/api/accounts). We also recommend you to install `bcrypt` node module, otherwise, you are going to see a warning saying that you are using a pure-Javascript implementation of it. diff --git a/v3-docs/docs/tutorials/svelte/8.deploying.md b/v3-docs/docs/tutorials/svelte/8.deploying.md index 352f8f7300..f436f67999 100644 --- a/v3-docs/docs/tutorials/svelte/8.deploying.md +++ b/v3-docs/docs/tutorials/svelte/8.deploying.md @@ -1,6 +1,6 @@ ## 8: Deploying -Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](https://guide.meteor.com/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. +Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](/tutorials/deployment/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. In this tutorial, we will deploy our app on [Galaxy](https://galaxycloud.app/), which is our own cloud solution. Galaxy offers a free plan, so you can deploy and test your app. Pretty cool, right? diff --git a/v3-docs/docs/tutorials/testing/testing.md b/v3-docs/docs/tutorials/testing/testing.md new file mode 100644 index 0000000000..e48e50f017 --- /dev/null +++ b/v3-docs/docs/tutorials/testing/testing.md @@ -0,0 +1,968 @@ +# Testing + +Testing allows you to ensure your application works the way you think it does, especially as your codebase changes over time. If you have good tests, you can refactor and rewrite code with confidence. Tests are also the most concrete form of documentation of expected behavior, since other developers can figure out how to use your code by reading the tests. + +Automated testing is critical because it allows you to run a far greater set of tests much more often than you could manually, allowing you to catch regression errors immediately. + +## Types of tests + +Entire books have been written on the subject of testing, so we will touch on some basics of testing here. The important thing to consider when writing a test is what part of the application you are trying to test, and how you are verifying the behavior works. + +- **Unit test**: If you are testing one small module of your application, you are writing a unit test. You'll need to *stub* and *mock* other modules that your module usually leverages in order to *isolate* each test. You'll typically also need to *spy* on actions that the module takes to verify that they occur. + +- **Integration test**: If you are testing that multiple modules behave properly in concert, you are writing an integration test. Such tests are much more complex and may require running code both on the client and on the server to verify that communication across that divide is working as expected. Typically an integration test will still isolate a part of the entire application and directly verify results in code. + +- **Acceptance test**: If you want to write a test that can be run against any running version of your app and verifies at the browser level that the right things happen when you push the right buttons, then you are writing an acceptance test (sometimes called "end to end test"). Such tests typically try to hook into the application as little as possible, beyond perhaps setting up the right data to run a test against. + +- **Load test**: Finally you may wish to test that your application works under typical load or see how much load it can handle before it falls over. This is called a load test or stress test. Such tests can be challenging to set up and typically aren't run often but are very important for confidence before a big production launch. + +## Challenges of testing in Meteor + +In most ways, testing a Meteor app is no different from testing any other full stack JavaScript application. However, compared to more traditional backend or front-end focused frameworks, two factors can make testing a little more challenging: + +- **Client/server data**: Meteor's data system makes it possible to bridge the client-server gap and often allows you to build your application without thinking about how data moves around. It becomes critical to test that your code does actually work correctly across that gap. In traditional frameworks where you spend a lot of time thinking about interfaces between client and server you can often get away with testing both sides of the interface in isolation, but Meteor's [full app test mode](#full-app-testing) makes it possible to write [integration tests](#full-app-integration-test) that cover the full stack. Another challenge here is creating test data in the client context; we'll discuss ways to do this in the [section on generating test data](#generating-test-data) below. + +- **Reactivity**: Meteor's reactivity system is "eventually consistent" in the sense that when you change a reactive input to the system, you'll see the user interface change to reflect this some time later. This can be a challenge when testing, but there are some ways to wait until those changes happen to verify the results, for example `Tracker.afterFlush()`. + +## The 'meteor test' command {#test-modes} + +The primary way to test your application in Meteor is the `meteor test` command. + +This loads your application in a special "test mode". What this does is: + +- *Doesn't* eagerly load *any* of our application code as Meteor normally would. + - *This is a highly important note as Meteor wouldn't know of any methods/collections/publications unless you import them in your test files.* +- *Does* eagerly load any file in our application (including in `imports/` folders) that look like `*.test[s].*`, or `*.spec[s].*` +- Sets the `Meteor.isTest` flag to be true. +- Starts up the test driver package ([see below](#driver-packages)). + +::: info +The Meteor build tool and the `meteor test` command ignore any files located in any `tests/` directory. This allows you to put tests in this directory that you can run using a test runner outside of Meteor's built-in test tools and still not have those files loaded in your application. See Meteor's [default file load order](/tutorials/application-structure/#load-order) rules. +::: + +What this means is that you can write tests in files with a certain filename pattern and know they'll not be included in normal builds of your app. When your app runs in test mode, those files will be loaded (and nothing else will), and they can import the modules you want to test. As we'll see this is ideal for [unit tests](#unit-testing) and [simple integration tests](#simple-integration-test). + +### Full-app testing {#full-app-testing} + +Additionally, Meteor offers a "full application" test mode. You can run this with `meteor test --full-app`. + +This is similar to test mode, with key differences: + +1. It loads test files matching `*.app-test[s].*` and `*.app-spec[s].*`. +2. It **does** eagerly load our application code as Meteor normally would. +3. Sets the `Meteor.isAppTest` flag to be true (instead of the `Meteor.isTest` flag). + +This means that the entirety of your application (including for instance the web server and client side router) is loaded and will run as normal. This enables you to write much more [complex integration tests](#full-app-integration-test) and also load additional files for [acceptance tests](#acceptance-testing). + +::: info +There is another test command in the Meteor tool; `meteor test-packages` is a way of testing Atmosphere packages, which is discussed in the [Writing Packages article](/packages/7.writing-atmosphere-packages). +::: + +### Driver packages + +When you run a `meteor test` command, you must provide a `--driver-package` argument. A test driver is a mini-application that runs in place of your app and runs each of your defined tests, whilst reporting the results in some kind of user interface. + +There are two main kinds of test driver packages: + +- **Web reporters**: Meteor applications that display a special test reporting web UI that you can view the test results in. + +- **Console reporters**: These run completely on the command-line and are primarily used for automated testing like [continuous integration](#ci). + +### Recommended: Mocha {#mocha} + +In this article, we'll use the popular [Mocha](https://mochajs.org) test runner. And you can pair it with any assertion library you want like [Chai](http://chaijs.com) or [expect](https://jestjs.io/docs/expect). In order to write and run tests in Mocha, we need to add an appropriate test driver package. + +There are several options. Choose the ones that makes sense for your app. You may depend on more than one and set up different test commands for different situations. + +* [meteortesting:mocha](https://atmospherejs.com/meteortesting/mocha) - Runs client and/or server package or app tests and reports all results in the server console. Supports various browsers for running client tests. Can be used for running tests on a CI server. Has a watch mode. + +These packages don't do anything in development or production mode. They declare themselves `testOnly` so they are not even loaded outside of testing. But when our app is run in [test mode](#test-modes), the test driver package takes over, executing test code on both the client and server, and rendering results to the browser. + +Here's how we can add the [`meteortesting:mocha`](https://atmospherejs.com/meteortesting/mocha) package to our app: + +```bash +meteor add meteortesting:mocha +``` + +## Test Files + +Test files themselves (for example a file named `todos-item.test.js` or `routing.app-specs.coffee`) can register themselves to be run by the test driver in the usual way for that testing library. For Mocha, that's by using `describe` and `it`: + +```js +describe('my module', function () { + it('does something that should be tested', function () { + // This code will be executed by the test driver when the app is started + // in the correct mode + }) +}) +``` + +::: warning +Arrow function use with Mocha [is discouraged](http://mochajs.org/#arrow-functions). +::: + +## Test data + +When your app is run in test mode, it is initialized with a clean test database. + +If you are running a test that relies on using the database, and specifically the content of the database, you'll need to perform some *setup* steps in your test to ensure the database is in the state you expect. + +```js +import { Meteor } from 'meteor/meteor'; +import expect from 'expect'; + +import { Notes } from './notes'; + +describe('notes', function () { + const noteOne = { + _id: 'testNote1', + title: 'Groceries', + body: 'Milk, Eggs and Oatmeal' + }; + + beforeEach(async function () { + await Notes.removeAsync({}); + await Notes.insertAsync(noteOne); + }); + // ... +}); +``` + +You can also use [`xolvio:cleaner`](https://atmospherejs.com/xolvio/cleaner) which is useful for resetting the entire database if you wish to do so. You can use it to reset the database in a `beforeEach` block: + +```js +import { resetDatabase } from 'meteor/xolvio:cleaner'; + +describe('my module', function () { + beforeEach(async function () { + await resetDatabase(); + }); +}); +``` + +This technique will only work on the server. If you need to reset the database from a client test, [`xolvio:cleaner`](https://github.com/xolvio/cleaner) provides you with a built-in method called `xolvio:cleaner/resetDatabase`: + +```js +describe('my module', function () { + beforeEach(async function () { + await Meteor.callAsync('xolvio:cleaner/resetDatabase'); + }); +}); +``` + +You can also invoke `resetDatabase` in your methods in case you wanted to apply custom code before or after: + +```js +import { resetDatabase } from 'meteor/xolvio:cleaner'; + +// NOTE: Before writing a method like this you'll want to double check +// that this file is only going to be loaded in test mode!! +Meteor.methods({ + async 'test.resetDatabase'() { + // custom code goes here... + await resetDatabase(); + // or here + } +}); +``` + +As we've placed the code above in a test file, it *will not* load in normal development or production mode (which would be an incredibly bad thing!). If you create an Atmosphere package with a similar feature, you should mark it as `testOnly` and it will similarly only load in test mode. + +### Generating test data + +Often it's sensible to create a set of data to run your test against. You can use standard `insertAsync()` calls against your collections to do this, but often it's easier to create *factories* which help encode random test data. A great package to use to do this is [`dburles:factory`](https://atmospherejs.com/dburles/factory). + +In the Todos example app, we define a factory to describe how to create a test todo item, using the [`faker`](https://www.npmjs.com/package/@faker-js/faker) npm package: + +```js +import { faker } from '@faker-js/faker'; + +Factory.define('todo', Todos, { + listId: () => Factory.get('list'), + text: () => faker.lorem.sentence(), + createdAt: () => new Date(), +}); +``` + +To use the factory in a test, we call `Factory.create`: + +```js +// This creates a todo and a list in the database and returns the todo. +const todo = await Factory.createAsync('todo'); + +// If we have a list already, we can pass in the id and avoid creating another: +const list = await Factory.createAsync('list'); +const todoInList = await Factory.createAsync('todo', { listId: list._id }); +``` + +### Mocking the database + +As `Factory.create` directly inserts documents into the collection that's passed into the `Factory.define` function, it can be a problem to use it on the client. However there's a neat isolation trick that you can do to replace the server-backed client collection with a mocked out local collection, that's encoded in the [`hwillson:stub-collections`](https://atmospherejs.com/hwillson/stub-collections) package. + +```js +import StubCollections from 'meteor/hwillson:stub-collections'; +import { Todos } from 'path/to/todos.js'; + +StubCollections.stub(Todos); + +// Now Todos is stubbed to a simple local collection mock, +// so for instance on the client we can do: +await Todos.insertAsync({ a: 'document' }); + +// Restore the `Todos` collection +StubCollections.restore(); +``` + +In a Mocha test, it makes sense to use `stub-collections` in a `beforeEach`/`afterEach` block. + +## Unit testing + +Unit testing is the process of isolating a section of code and then testing that the internals of that section work as you expect. As we've split our code base up into ES2015 modules it's natural to test those modules one at a time. + +By isolating a module and testing its internal functionality, we can write tests that are *fast* and *accurate*---they can quickly tell you where a problem in your application lies. Note however that incomplete unit tests can often hide bugs because of the way they stub out dependencies. For that reason it's useful to combine unit tests with slower (and perhaps less commonly run) integration and acceptance tests. + +### A simple React unit test + +We recommend the [React Testing Library](https://testing-library.com/docs/react-testing-library/intro/) for testing React components, which provides utilities to test components in a way that resembles how users interact with them. + +```js +import { Factory } from 'meteor/dburles:factory'; +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { expect } from 'chai'; +import TodoItem from './TodoItem.jsx'; + +describe('TodoItem', () => { + it('should render', () => { + const todo = Factory.build('todo', { text: 'testing', checked: false }); + render(); + + expect(screen.getByRole('textbox')).to.have.value('testing'); + expect(screen.queryByRole('checkbox')).not.to.be.checked; + }); +}); +``` + +And here's an example of simulating a user checking the todo item: + +```js +import { Factory } from 'meteor/dburles:factory'; +import React from 'react'; +import { render, screen, fireEvent } from '@testing-library/react'; +import sinon from 'sinon'; +import TodoItem from './TodoItem.jsx'; +import { setCheckedStatus } from '../../api/todos/methods.js'; + +describe('TodoItem', () => { + it('should update status when checked', async () => { + sinon.stub(setCheckedStatus, 'callAsync'); + const todo = await Factory.createAsync('todo', { checked: false }); + render(); + + fireEvent.click(screen.getByRole('checkbox')); + + sinon.assert.calledWith(setCheckedStatus.callAsync, { + todoId: todo._id, + newCheckedStatus: true, + }); + + setCheckedStatus.callAsync.restore(); + }); +}); +``` + +In this case, the `TodoItem` component calls a Meteor Method `setCheckedStatus` when the user clicks, but this is a unit test so there's no server running. So we stub it out using [Sinon](http://sinonjs.org). After we simulate the click, we verify that the stub was called with the correct arguments. Finally, we clean up the stub and restore the original method behavior. + +### A simple Blaze unit test + +To unit test Blaze components, we'll use a very simple test helper that renders a Blaze component off-screen with a given data context. + +`imports/ui/test-helpers.js`: + +```js +import isString from 'lodash.isstring'; +import { Template } from 'meteor/templating'; +import { Blaze } from 'meteor/blaze'; +import { Tracker } from 'meteor/tracker'; + +const withDiv = function withDiv(callback) { + const el = document.createElement('div'); + document.body.appendChild(el); + let view = null; + try { + view = callback(el); + } finally { + if (view) Blaze.remove(view); + document.body.removeChild(el); + } +}; + +export const withRenderedTemplate = function withRenderedTemplate(template, data, callback) { + withDiv((el) => { + const ourTemplate = isString(template) ? Template[template] : template; + const view = Blaze.renderWithData(ourTemplate, data, el); + Tracker.flush(); + callback(el); + return view; + }); +}; +``` + +Here's what a unit test looks like: + +`imports/ui/components/client/todos-item.tests.js`: + +```js +/* eslint-env mocha */ + +import { Factory } from 'meteor/dburles:factory'; +import chai from 'chai'; +import { Template } from 'meteor/templating'; +import $ from 'jquery'; +import { Todos } from '../../../api/todos/todos'; + +import { withRenderedTemplate } from '../../test-helpers.js'; +import '../todos-item.js'; + +describe('Todos_item', function () { + beforeEach(function () { + Template.registerHelper('_', key => key); + }); + + afterEach(function () { + Template.deregisterHelper('_'); + }); + + it('renders correctly with simple data', function () { + const todo = Factory.build('todo', { checked: false }); + const data = { + todo: Todos._transform(todo), + onEditingChange: () => 0, + }; + + withRenderedTemplate('Todos_item', data, el => { + chai.assert.equal($(el).find('input[type=text]').val(), todo.text); + chai.assert.equal($(el).find('.list-item.checked').length, 0); + chai.assert.equal($(el).find('.list-item.editing').length, 0); + }); + }); +}); +``` + +#### Importing + +When we run our app in test mode, only our test files will be eagerly loaded. In particular, this means that in order to use our templates, we need to import them! In this test, we import `todos-item.js`, which itself imports `todos.html` (yes, you do need to import the HTML files of your Blaze templates!) + +#### Stubbing + +To be a unit test, we must stub out the dependencies of the module. In this case, thanks to the way we've isolated our code into a reusable component, there's not much to do; principally we need to stub out the `{{_}}` helper that's created by the internationalization system. Note that we stub it out in a `beforeEach` and restore it in the `afterEach`. + +#### Creating data + +We can use the Factory package's `.build()` API to create a test document without inserting it into any collection. As we've been careful not to call out to any collections directly in the reusable component, we can pass the built `todo` document directly into the template. + +### Running unit tests + +To run the tests that our app defines, we run our app in [test mode](#test-modes): + +```bash +TEST_WATCH=1 meteor test --driver-package meteortesting:mocha +``` + +As we've defined a test file (`imports/todos/todos.tests.js`), what this means is that the file above will be eagerly loaded, adding the `'builds correctly from factory'` test to the Mocha registry. + +To run the tests, visit http://localhost:3000 in your browser. This kicks off `meteortesting:mocha`, which runs your tests both in the browser and on the server. It will display the test results in a div with ID mocha. + +Usually, while developing an application, it makes sense to run `meteor test` on a second port (say `3100`), while also running your main application in a separate process: + +```bash +# in one terminal window +meteor + +# in another +meteor test --driver-package meteortesting:mocha --port 3100 +``` + +Then you can open two browser windows to see the app in action while also ensuring that you don't break any tests as you make changes. + +### Isolation techniques + +In the unit tests above we saw a very limited example of how to isolate a module from the larger app. This is critical for proper unit testing. Some other utilities and techniques include: + + - The [`velocity:meteor-stubs`](https://atmospherejs.com/velocity/meteor-stubs) package, which creates simple stubs for most Meteor core objects. + + - Alternatively, you can also use tools like [Sinon](http://sinonjs.org) to stub things directly, as we'll see for example in our [simple integration test](#simple-integration-test). + + - The [`hwillson:stub-collections`](https://atmospherejs.com/hwillson/stub-collections) package we mentioned [above](#mocking-the-database). + +### Testing publications + +Let's take this simple publication for example: + +```js +// server/publications/notes +Meteor.publish('user.notes', function () { + return Notes.find({ userId: this.userId }); +}); +``` + +We access Meteor publications using `Meteor.server.publish_handlers`, then use `.apply` to provide the needed parameters for the publication and test what it returns. + +```js +import { Meteor } from 'meteor/meteor'; +import expect from 'expect'; + +import { Notes } from './notes'; + +describe('notes', function () { + const noteOne = { + _id: 'testNote1', + title: 'Groceries', + body: 'Milk, Eggs and Oatmeal', + userId: 'userId1' + }; + + beforeEach(async function () { + await Notes.removeAsync({}); + await Notes.insertAsync(noteOne); + }); + + it('should return a users notes', async function () { + const res = Meteor.server.publish_handlers['user.notes'].apply({ userId: noteOne.userId }); + const notes = await res.fetchAsync(); + + expect(notes.length).toBe(1); + expect(notes[0]).toEqual(noteOne); + }); + + it('should return no notes for user that has none', async function () { + const res = Meteor.server.publish_handlers['user.notes'].apply({ userId: 'testid' }); + const notes = await res.fetchAsync(); + + expect(notes.length).toBe(0); + }); +}); +``` + +A useful package for testing publications is [`johanbrook:publication-collector`](https://atmospherejs.com/johanbrook/publication-collector), it allows you to test individual publication's output without needing to create a traditional subscription: + +```js +describe('notes', function () { + it('should return a users notes', async function () { + const collector = new PublicationCollector({ userId: noteOne.userId }); + + const collections = await collector.collect('user.notes'); + chai.assert.typeOf(collections.Notes, 'array'); + chai.assert.equal(collections.Notes.length, 1); + }); +}); +``` + +### Testing methods + +We can also access methods using `Meteor.server.method_handlers` and apply the same principles. Take note of how we can use `sinon.fake()` to mock `this.unblock()`. + +```js +Meteor.methods({ + async 'notes.insert'(title, body) { + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You have to be authorized'); + } + + check(title, String); + check(body, String); + + this.unblock(); + + return await Notes.insertAsync({ + title, + body, + userId: this.userId + }); + }, + async 'notes.remove'(_id) { + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You have to be authorized'); + } + + check(_id, String); + + await Notes.removeAsync({ _id, userId: this.userId }); + }, + async 'notes.update'(_id, { title, body }) { + if (!this.userId) { + throw new Meteor.Error('not-authorized', 'You have to be authorized'); + } + + check(_id, String); + check(title, String); + check(body, String); + + await Notes.updateAsync({ + _id, + userId: this.userId + }, { + $set: { title, body } + }); + } +}); +``` + +```js +describe('notes', function () { + const noteOne = { + _id: 'testNote1', + title: 'Groceries', + body: 'Milk, Eggs and Oatmeal', + userId: 'testUserId1' + }; + + beforeEach(async function () { + await Notes.removeAsync({}); + }); + + it('should insert new note', async function () { + const _id = await Meteor.server.method_handlers['notes.insert'].apply( + { userId: noteOne.userId, unblock: sinon.fake() }, + [noteOne.title, noteOne.body] + ); + + const note = await Notes.findOneAsync({ _id }); + expect(note).toMatchObject( + expect.objectContaining({ title: noteOne.title, body: noteOne.body }) + ); + }); + + it('should not insert note if not authenticated', async function () { + await expect(async () => { + await Meteor.server.method_handlers['notes.insert'].apply({}, []); + }).rejects.toThrow(); + }); + + it('should remove note', async function () { + await Notes.insertAsync(noteOne); + await Meteor.server.method_handlers['notes.remove'].apply( + { userId: noteOne.userId }, + [noteOne._id] + ); + + const note = await Notes.findOneAsync({ _id: noteOne._id }); + expect(note).toBeUndefined(); + }); + + it('should update note', async function () { + await Notes.insertAsync(noteOne); + const title = 'To Buy'; + const body = 'Beef, Salmon'; + + await Meteor.server.method_handlers['notes.update'].apply( + { userId: noteOne.userId }, + [noteOne._id, { title, body }] + ); + + const note = await Notes.findOneAsync(noteOne._id); + expect(note.title).toBe(title); + expect(note.body).toBe(body); + }); +}); +``` + +## Integration testing + +An integration test is a test that crosses module boundaries. In the simplest case, this means something very similar to a unit test, where you perform your isolation around multiple modules, creating a non-singular "system under test". + +Although conceptually different to unit tests, such tests typically do not need to be run any differently to unit tests and can use the same [`meteor test` mode](#running-unit-tests) and [isolation techniques](#isolation-techniques) as we use for unit tests. + +However, an integration test that crosses the client-server boundary of a Meteor application (where the modules under test cross that boundary) requires a different testing infrastructure, namely Meteor's "full app" testing mode. + +### Simple integration test + +Our reusable components were a natural fit for a unit test; similarly our smart components tend to require an integration test to really be exercised properly, as the job of a smart component is to bring data together and supply it to a reusable component. + +Here's an example of a simple integration test for a smart component: + +```js +/* eslint-env mocha */ + +import { Meteor } from 'meteor/meteor'; +import { Factory } from 'meteor/dburles:factory'; +import { Random } from 'meteor/random'; +import chai from 'chai'; +import StubCollections from 'meteor/hwillson:stub-collections'; +import { Template } from 'meteor/templating'; +import $ from 'jquery'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import sinon from 'sinon'; + +import { withRenderedTemplate } from '../../test-helpers.js'; +import '../lists-show-page.js'; + +import { Todos } from '../../../api/todos/todos.js'; +import { Lists } from '../../../api/lists/lists.js'; + +describe('Lists_show_page', function () { + const listId = Random.id(); + + beforeEach(function () { + StubCollections.stub([Todos, Lists]); + Template.registerHelper('_', key => key); + sinon.stub(FlowRouter, 'getParam').returns(listId); + sinon.stub(Meteor, 'subscribe').returns({ + subscriptionId: 0, + ready: () => true, + }); + }); + + afterEach(function () { + StubCollections.restore(); + Template.deregisterHelper('_'); + FlowRouter.getParam.restore(); + Meteor.subscribe.restore(); + }); + + it('renders correctly with simple data', async function () { + await Factory.createAsync('list', { _id: listId }); + const timestamp = new Date(); + const todos = []; + for (let i = 0; i < 3; i++) { + todos.push(await Factory.createAsync('todo', { + listId, + createdAt: new Date(timestamp - (3 - i)), + })); + } + + withRenderedTemplate('Lists_show_page', {}, el => { + const todosText = todos.map(t => t.text).reverse(); + const renderedText = $(el).find('.list-items input[type=text]') + .map((i, e) => $(e).val()) + .toArray(); + chai.assert.deepEqual(renderedText, todosText); + }); + }); +}); +``` + +### Full-app integration test + +In a full-app integration test, we test that the application works correctly end-to-end. + +```js +/* eslint-env mocha */ + +import { Meteor } from 'meteor/meteor'; +import { Tracker } from 'meteor/tracker'; +import { DDP } from 'meteor/ddp-client'; +import { FlowRouter } from 'meteor/ostrio:flow-router-extra'; +import { assert } from 'chai'; +import $ from 'jquery'; + +import { generateData } from './../../api/generate-data.app-tests.js'; +import { Lists } from '../../api/lists/lists.js'; +import { Todos } from '../../api/todos/todos.js'; + +// Utility -- returns a promise which resolves when all subscriptions are done +const waitForSubscriptions = () => new Promise(resolve => { + const poll = Meteor.setInterval(() => { + if (DDP._allSubscriptionsReady()) { + Meteor.clearInterval(poll); + resolve(); + } + }, 200); +}); + +// Tracker.afterFlush runs code when all consequent of a tracker based change +// (such as a route change) have occured. This makes it a promise. +const afterFlushPromise = () => new Promise(resolve => Tracker.afterFlush(resolve)); + +if (Meteor.isClient) { + describe('data available when routed', () => { + // First, ensure the data that we expect is loaded on the server + // Then, route the app to the homepage + beforeEach(async () => { + await generateData(); + FlowRouter.go('/'); + await waitForSubscriptions(); + }); + + describe('when logged out', () => { + it('has all public lists at homepage', async () => { + assert.equal(await Lists.find().countAsync(), 3); + }); + + it('renders the correct list when routed to', async () => { + const list = await Lists.findOneAsync(); + FlowRouter.go('Lists.show', { _id: list._id }); + + await afterFlushPromise(); + await waitForSubscriptions(); + + assert.equal($('.title-wrapper').html(), list.name); + assert.equal(await Todos.find({ listId: list._id }).countAsync(), 3); + }); + }); + }); +} +``` + +Of note here: + +- Before running, each test sets up the data it needs using the `generateData` helper (see [the section on creating integration test data](#creating-integration-test-data) for more detail) then goes to the homepage. + +- Although Flow Router doesn't take a done callback, we can use `Tracker.afterFlush` to wait for all its reactive consequences to occur. + +- Here we wrote a little utility to wait for all the subscriptions which are created by the route change to become ready before checking their data. + +### Running full-app tests + +To run the full-app tests in our application, we run: + +```bash +meteor test --full-app --driver-package meteortesting:mocha +``` + +When we connect to the test instance in a browser, we want to render a testing UI rather than our app UI, so the test driver package will hide any UI of our application and overlay it with its own. However the app continues to behave as normal, so we are able to route around and check the correct data is loaded. + +### Creating integration test data + +To create test data in full-app test mode, it usually makes sense to create some special test methods which we can call from the client side. Usually when testing a full app, we want to make sure the publications are sending through the correct data (as we do in this test), and so it's not sufficient to stub out the collections and place synthetic data in them. Instead we'll want to actually create data on the server and let it be published. + +Similar to the way we cleared the database using a method in the `beforeEach` in the [test data](#test-data) section above, we can call a method to do that before running our tests: + +`imports/api/generate-data.app-tests.js`: + +```js +// This file will be auto-imported in the app-test context, +// ensuring the method is always available + +import { Meteor } from 'meteor/meteor'; +import { Factory } from 'meteor/dburles:factory'; +import { resetDatabase } from 'meteor/xolvio:cleaner'; +import { Random } from 'meteor/random'; + +const createList = async (userId) => { + const list = await Factory.createAsync('list', { userId }); + for (let i = 0; i < 3; i++) { + await Factory.createAsync('todo', { listId: list._id }); + } + return list; +}; + +// Remember to double check this is a test-only file before +// adding a method like this! +Meteor.methods({ + async generateFixtures() { + await resetDatabase(); + + // create 3 public lists + for (let i = 0; i < 3; i++) { + await createList(); + } + + // create 3 private lists + for (let i = 0; i < 3; i++) { + await createList(Random.id()); + } + }, +}); + +let generateData; +if (Meteor.isClient) { + // Create a second connection to the server to use to call + // test data methods. We do this so there's no contention + // with the currently tested user's connection. + const testConnection = Meteor.connect(Meteor.absoluteUrl()); + + generateData = () => { + return new Promise((resolve, reject) => { + testConnection.call('generateFixtures', (err) => { + if (err) reject(err); + else resolve(); + }); + }); + }; +} + +export { generateData }; +``` + +Note that we've exported a client-side symbol `generateData` which is a promisified version of the method call, which makes it simpler to use this sequentially in tests. + +Also of note is the way we use a second DDP connection to the server in order to send these test "control" method calls. + +## Acceptance testing + +Acceptance testing is the process of taking an unmodified version of our application and testing it from the "outside" to make sure it behaves in a way we expect. Typically if an app passes acceptance tests, we have done our job properly from a product perspective. + +As acceptance tests test the behavior of the application in a full browser context in a generic way, there are a range of tools that you can use to specify and run such tests. In this guide we'll demonstrate using [Cypress](https://www.cypress.io/), an acceptance testing tool with a few neat Meteor-specific features that makes it easy to use. + +Install Cypress as a dev dependency: + +```bash +cd /your/project/path +meteor npm install cypress --save-dev +``` + +Designate a special directory for cypress tests to avoid Meteor eagerly loading it: + +```bash +mkdir tests +mv cypress/ tests/cypress +``` + +Create `cypress.config.js` file at the root of your project to configure Cypress: + +```js +const { defineConfig } = require('cypress'); + +module.exports = defineConfig({ + e2e: { + baseUrl: 'http://localhost:3000', + fixturesFolder: 'tests/cypress/fixtures', + specPattern: 'tests/cypress/e2e/**/*.cy.{js,jsx,ts,tsx}', + screenshotsFolder: 'tests/cypress/screenshots', + videosFolder: 'tests/cypress/videos', + supportFile: 'tests/cypress/support/e2e.js', + }, +}); +``` + +Add commands to your `package.json`: + +```json +{ + "scripts": { + "cypress:open": "cypress open", + "cypress:run": "cypress run" + } +} +``` + +Now, let's create a simple test by adding a new file called `signup.cy.js` in the `tests/cypress/e2e/` directory: + +```js +describe('sign-up', () => { + beforeEach(() => { + cy.visit('/'); + }); + + it('should create and log the new user', () => { + cy.contains('Register').click(); + cy.get('input#at-field-email').type('jean-peter.mac.calloway@gmail.com'); + cy.get('input#at-field-password').type('awesome-password'); + cy.get('input#at-field-password_again').type('awesome-password'); + // I added a name field on meteor user accounts system + cy.get('input#at-field-name').type('Jean-Peter'); + cy.get('button#at-btn').click(); + + cy.url().should('eq', 'http://localhost:3000/board'); + + cy.window().then(win => { + // this allows accessing the window object within the browser + const user = win.Meteor.user(); + expect(user).to.exist; + expect(user.profile.name).to.equal('Jean-Peter'); + expect(user.emails[0].address).to.equal( + 'jean-peter.mac.calloway@gmail.com' + ); + }); + }); +}); +``` + +## Continuous Integration {#ci} + +Continuous integration testing is the process of running tests on every commit of your project. + +There are two principal ways to do it: on the developer's machine before allowing them to push code to the central repository, and on a dedicated CI server after each push. Both techniques are useful, and both require running tests in a commandline-only fashion. + +### Command line + +We've seen one example of running tests on the command line, using our `meteor npm run cypress:run` mode. + +We can also use a command-line driver for Mocha [`meteortesting:mocha`](https://atmospherejs.com/meteortesting/mocha) to run our standard tests on the command line. + +Adding and using the package is straightforward: + +```bash +meteor add meteortesting:mocha +meteor test --once --driver-package meteortesting:mocha +``` + +(The `--once` argument ensures the Meteor process stops once the test is done). + +We can also add that command to our `package.json` as a `test` script: + +```json +{ + "scripts": { + "test": "meteor test --once --driver-package meteortesting:mocha" + } +} +``` + +Now we can run the tests with `meteor npm test`. + +### GitHub Actions + +[GitHub Actions](https://github.com/features/actions) is a great continuous integration service that allows us to run tests on every push to a repository. Here's an example workflow file: + +`.github/workflows/test.yml`: + +```yaml +name: Test + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Meteor + run: | + curl https://install.meteor.com/ | sh + + - name: Install dependencies + run: meteor npm install + + - name: Run tests + run: meteor npm test +``` + +### CircleCI + +[CircleCI](https://circleci.com) is another great continuous integration service. Here's an example configuration: + +`.circleci/config.yml`: + +```yaml +version: 2.1 + +jobs: + test: + docker: + - image: cimg/node:20.0 + steps: + - checkout + - run: + name: Install Meteor + command: curl https://install.meteor.com/ | sh + - run: + name: Install dependencies + command: meteor npm install + - run: + name: Run tests + command: meteor npm test + +workflows: + test: + jobs: + - test +``` diff --git a/v3-docs/docs/tutorials/vue/meteorjs3-vue3.md b/v3-docs/docs/tutorials/vue/meteorjs3-vue3.md index 25a0f615bd..f6ec9f037c 100644 --- a/v3-docs/docs/tutorials/vue/meteorjs3-vue3.md +++ b/v3-docs/docs/tutorials/vue/meteorjs3-vue3.md @@ -26,7 +26,7 @@ First, ensure you have Node.js version 20 installed. You can install the latest npx meteor ``` -If you encounter any issues, please refer to the requirements and details in [our documentation](https://v3-docs.meteor.com/about/install.html). +If you encounter any issues, please refer to the requirements and details in [our documentation](/about/install). ### 1.2: Create a Meteor.js Project @@ -194,7 +194,7 @@ In the next step, we will connect to the MongoDB database to store our tasks. Meteor already sets up MongoDB for you. In order to use our database we need to create a *collection*, which is where we will store our *documents*, in our case our `tasks`. -You can read more about collections [here](http://guide.meteor.com/collections.html). +You can read more about collections [here](/tutorials/collections/collections). In this step we will implement all the necessary code to have a basic collection for our tasks up and running. @@ -213,7 +213,7 @@ export const TasksCollection = new Mongo.Collection('tasks'); ``` ::: -The code above instantiates a new MongoDB collection and exports it. You can read more about app structure and imports/exports [here](https://guide.meteor.com/structure.html). +The code above instantiates a new MongoDB collection and exports it. You can read more about app structure and imports/exports [here](/tutorials/application-structure/). ### 2.2: Initialize Tasks Collection @@ -416,7 +416,7 @@ const tasks = autorun(() => TasksCollection.find({}).fetch()).result; Now you can edit the `addTask` function to insert a new task into the database. To do it, we will need to implement a Meteor Method. -Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](https://guide.meteor.com/methods.html). +Methods are essentially RPC calls to the server that let you perform operations on the server side securely. You can read more about Meteor Methods [here](/tutorials/methods/methods). To create your methods, you can create a file called `tasksMethods.js`. @@ -475,7 +475,7 @@ Inside the function, we are adding a task to the `tasks` collection by calling ` ### 3.5: Show Newest Tasks First -Now, you just need to make a change which will improve user experience: we will show the newest tasks first. We can accomplish this quickly by sorting our [MongoDB](https://guide.meteor.com/collections.html#mongo-collections) query. +Now, you just need to make a change which will improve user experience: we will show the newest tasks first. We can accomplish this quickly by sorting our [MongoDB](/tutorials/collections/collections#mongo-collections) query. ::: code-group @@ -865,7 +865,7 @@ Meteor already comes with authentication and account management system out of th meteor add accounts-password ``` -> There are many more authentication methods supported. You can read more about the accounts system [here](https://docs.meteor.com/api/accounts.html). +> There are many more authentication methods supported. You can read more about the accounts system [here](/api/accounts). @@ -1260,7 +1260,7 @@ Your app should look like this: ## 8: Deploying -Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](https://guide.meteor.com/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. +Deploying a Meteor application is similar to deploying any other Node.js app that uses websockets. You can find deployment options in [our guide](/tutorials/deployment/deployment), including Meteor Up, Docker, and our recommended method, Galaxy. In this tutorial, we will deploy our app on [Galaxy](https://www.meteor.com/hosting), which is our own cloud solution. Galaxy offers a free plan, so you can deploy and test your app. Pretty cool, right?