diff --git a/website/docs/__partials__/node/package-workspaces.mdx b/website/docs/__partials__/node/package-workspaces.mdx new file mode 100644 index 00000000000..1c889221601 --- /dev/null +++ b/website/docs/__partials__/node/package-workspaces.mdx @@ -0,0 +1,66 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +```json title="package.json" +{ + // ... + "workspaces": ["apps/*", "packages/*"] +} +``` + +```yaml title=".yarnrc.yml" +# ... +nodeLinker: 'node-modules' +``` + +- [Documentation](https://yarnpkg.com/features/workspaces) + + + + +```json title="package.json" +{ + // ... + "workspaces": ["apps/*", "packages/*"] +} +``` + +- [Documentation](https://classic.yarnpkg.com/en/docs/workspaces) + + + + +```json title="package.json" +{ + // ... + "workspaces": ["apps/*", "packages/*"] +} +``` + +- [Documentation](https://docs.npmjs.com/cli/v8/using-npm/workspaces) + + + + +```yaml title="pnpm-workspace.yaml" +packages: + - 'apps/*' + - 'packages/*' +``` + +- [Documentation](https://pnpm.io/workspaces) + + + diff --git a/website/docs/guides/examples/sveltekit.mdx b/website/docs/guides/examples/sveltekit.mdx index c4cac7b29d7..fc3e7199385 100644 --- a/website/docs/guides/examples/sveltekit.mdx +++ b/website/docs/guides/examples/sveltekit.mdx @@ -5,6 +5,9 @@ sidebar_label: SvelteKit import AddDepsTabs from '@site/src/components/AddDepsTabs'; import CreateDepTabs from '@site/src/components/CreateDepTabs'; +import HeadingApiLink from '@site/src/components/Docs/HeadingApiLink'; + + [SvelteKit](https://kit.svelte.dev) is built on [Svelte](https://svelte.dev), a UI framework that uses a compiler to let you write breathtakingly concise components that do minimal work in the @@ -43,14 +46,8 @@ tasks: command: 'vite dev' local: true - lint: - command: 'eslint .' - - # Production build (SSR) build: command: 'vite build' - deps: - - '~:lint' inputs: - '@group(svelte)' outputs: @@ -64,11 +61,8 @@ tasks: test: command: 'vitest' - - playwright: - command: 'playwright test' inputs: - - '@globs(tests)' + - '@group(svelte)' ``` To run the SvelteKit dev server: @@ -80,12 +74,31 @@ moon run :dev ### ESLint integration SvelteKit provides an option to setup ESLint along with your project, with moon you can use a -[global `lint` task](./eslint). +[global `lint` task](./eslint). We encourage using the global `lint` task for consistency across all +projects within the repository. With this approach, the `eslint` command itself will be ran and the +`svelte3` rules will still be used. -We encourage using the global `lint` task for consistency across all projects within the repository. -With this approach, the `eslint` command itself will be ran and the `svelte3` rules will still be -used. Additionally, we suggest disabling the linter during the build process, but is not a -requirement. As a potential alternative, add the `lint` task as a dependency for the `build` task. +```yaml title="/moon.yml" +tasks: + # Extends the top-level lint + lint: + args: + - '--ext' + - '.ts,.svelte' +``` + +Be sure to enable the Svelte parser and plugin in a project local ESLint configuration file. + +```js title=".eslintrc.cjs" +module.exports = { + plugins: ['svelte3'], + ignorePatterns: ['*.cjs'], + settings: { + 'svelte3/typescript': () => require('typescript'), + }, + overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], +}; +``` ### TypeScript integration @@ -96,11 +109,26 @@ continuing. At this point we'll assume that a `tsconfig.json` has been created in the application, and typechecking works. From here we suggest utilizing a [global `typecheck` task](./typescript) for -consistency across all projects within the repository. +consistency across all projects within the repository. However, because Svelte isn't standard +JavaScript, it requires the use of the `svelte-check` command for type-checking. -Additionally, we suggest disabling the typechecker during the build process, but is not a -requirement. As a potential alternative, add the `typecheck` task as a dependency for the `build` -task. +```yaml title="/moon.yml" +workspace: + inheritedTasks: + exclude: ['typecheck'] + +tasks: + typecheck-sync: + command: 'svelte-kit sync' + + typecheck: + command: 'svelte-check --tsconfig ./tsconfig.json' + deps: + - 'typecheck-sync' + inputs: + - '@group(svelte)' + - 'tsconfig.json' +``` In case Svelte doesn't automatically create a `tsconfig.json`, you can use the following: diff --git a/website/docs/guides/javascript/__partials__/workspace-commands.mdx b/website/docs/guides/javascript/__partials__/workspace-commands.mdx new file mode 100644 index 00000000000..be2823f7d9b --- /dev/null +++ b/website/docs/guides/javascript/__partials__/workspace-commands.mdx @@ -0,0 +1,154 @@ +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + + +Install dependencies: + +```shell +npm install +``` + +Add a package: + +```shell +# At the root +npm install + +# In a project +npm install --workspace +``` + +Remove a package: + +```shell +# At the root +npm install + +# In a project +npm install --workspace +``` + +Update packages: + +```shell +npx npm-check-updates --interactive +``` + + + + +Install dependencies: + +```shell +pnpm install +``` + +Add a package: + +```shell +# At the root +pnpm add + +# In a project +pnpm add --filter +``` + +Remove a package: + +```shell +# At the root +pnpm remove + +# In a project +pnpm remove --filter +``` + +Update packages: + +```shell +pnpm update -i -r --latest +``` + + + + +Install dependencies: + +```shell +yarn install +``` + +Add a package: + +```shell +# At the root +yarn add + +# In a project +yarn workspace add +``` + +Remove a package: + +```shell +# At the root +yarn remove + +# In a project +yarn workspace remove +``` + +Update packages: + +```shell +yarn upgrade-interactive +``` + + + + +Install dependencies: + +```shell +yarn install +``` + +Add a package: + +```shell +# At the root +yarn add -w + +# In a project +yarn workspace add +``` + +Remove a package: + +```shell +# At the root +yarn remove -w + +# In a project +yarn workspace remove +``` + +Update packages: + +```shell +yarn upgrade-interactive --latest +``` + + + diff --git a/website/docs/guides/javascript/node-handbook.mdx b/website/docs/guides/javascript/node-handbook.mdx new file mode 100644 index 00000000000..2102c3d349e --- /dev/null +++ b/website/docs/guides/javascript/node-handbook.mdx @@ -0,0 +1,390 @@ +--- +title: Node.js handbook +toc_max_heading_level: 6 +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +Utilizing JavaScript in a monorepo can be a daunting task, especially when using Node.js, as there +are many ways to structure your code and to configure your tools. With this handbook, we'll help +guide you through this process. + +:::info + +This guide is a living document and will continue to be updated over time! + +::: + +## moon setup + +For this part of the handbook, we'll be focusing on [moon](/moon), our build system. To start, +languages in moon act like plugins, where their functionality and support _is not_ enabled unless +explicitly configured. We follow this approach to avoid unnecessary overhead. + +### Enabling the language + +To enable JavaScript support via Node.js, define the [`node`](../../config/toolchain#node) setting +in [`.moon/toolchain.yml`](../../config/toolchain), even if an empty object. + +```yaml title=".moon/toolchain.yml" +# Enable Node.js +node: {} + +# Enable Node.js and override default settings +node: + packageManager: 'pnpm' +``` + +This will enable the Node.js platform and provide the following automations around its ecosystem: + +- Node modules will automatically be installed if dependencies in `package.json` have changed, or + the lockfile has changed, since the last time a task has ran. + - We'll also take `package.json` workspaces into account and install modules in the correct + location; either the workspace root, in a project, or both. +- Relationships between projects will automatically be discovered based on `dependencies`, + `devDependencies`, and `peerDependencies` in `package.json`. + - The versions of these packages will also be automatically synced when changed. +- Tasks can be [automatically inferred](../../config/toolchain#infertasksfromscripts) from + `package.json` scripts. +- And much more! + +### Utilizing the toolchain + +When a language is enabled, moon by default will assume that the language's binary is available +within the current environment (typically on `PATH`). This has the downside of requiring all +developers and machines to manually install the correct version of the language, _and to stay in +sync_. + +Instead, you can utilize [moon's toolchain](../../concepts/toolchain), which will download and +install the language in the background, and ensure every task is executed using the exact version +across all machines. + +Enabling the toolchain is as simple as defining the [`node.version`](../../config/toolchain#version) +setting. + +```yaml title=".moon/toolchain.yml" +# Enable Node.js toolchain with an explicit version +node: + version: '18.0.0' +``` + +### Using `package.json` names and scripts + +If you're looking to prototype moon, or reduce the migration effort to moon tasks, you can configure +moon to inherit `package.json` scripts, and internally convert them to moon tasks. This can be +achieved with the [`node.inferTasksFromScripts`](../../config/toolchain#infertasksfromscripts) +setting. + +```yaml title=".moon/toolchain.yml" +node: + inferTasksFromScripts: true +``` + +Furthermore, if you'd like to reference projects by their package name (both in configuration and on +the command line), instead of the moon project name, enable the +[`node.aliasPackageNames`](../../config/toolchain#aliaspackagenames) setting. + +```yaml title=".moon/toolchain.yml" +node: + aliasPackageNames: 'name-and-scope' +``` + +> These are not enabled by default as we want to encourage the +> [use of moon tasks](../../faq#can-we-use-packagejson-scripts). + +## Repository structure + +JavaScript monorepo's work best when projects are split into applications and packages, with each +project containing its own `package.json` and dependencies. A root `package.json` must also exist +that pieces all projects together through workspaces. + +For small repositories, the following structure typically works well: + +``` +/ +├── .moon/ +├── package.json +├── apps/ +│ └── client/ +| ├── ... +│ └── package.json +│ └── server/ +| ├── ... +│ └── package.json +└── packages/ + ├── components/ + | ├── ... + │ └── package.json + ├── theme/ + | ├── ... + │ └── package.json + └── utils/ + ├── ... + └── package.json +``` + +For large repositories, grouping projects by team or department helps with ownership and +organization. With this structure, applications and libraries can be nested at any depth. + +``` +/ +├── .moon/ +├── package.json +├── infra/ +│ └── ... +├── internal/ +│ └── ... +├── payments/ +│ └── ... +└── shared/ + └── ... +``` + +### Applications + +Applications are runnable or executable, like an HTTP server, and are pieced together with packages +and its own encapsulated code. They represent the whole, while packages are the pieces. Applications +can import and depend on packages, but they _must not_ import and depend on other applications. + +In moon, you can denote a project as an application using the [`type`](../../config/project#type) +setting in [`moon.yml`](../../config/project). + +```yaml title="moon.yml" +type: 'application' +``` + +### Packages + +Packages (also known as a libraries) are self-contained reusable pieces of code, and are the +suggested pattern for [code sharing](#code-sharing). Packages can import and depend on other +packages, but they _must not_ import and depend on applications! + +In moon, you can denote a project as a library using the [`type`](../../config/project#type) setting +in [`moon.yml`](../../config/project). + +```yaml title="moon.yml" +type: 'library' +``` + +### Configuration + +Every tool that you'll utilize in a repository will have its own configuration file. This will be a +lot of config files, but regardless of what tool it is, where the config file should go will fall +into 1 of these categories: + +- **Settings are inherited by all projects.** These are known as universal tools, and enforce code + consistency and quality across the entire repository. Their config file must exist in the + repository root, but may support overrides in each project. + - Examples: Babel, [ESLint](../examples/eslint), [Prettier](../examples/prettier), + [TypeScript](../examples/typescript) +- **Settings are unique per project.** These are developers tools that must be configured separately + for each project, as they'll have different concerns. Their config file must exist in each + project, but a shared configuration may exist as a base (for example, Jest presets). + - Examples: [Jest](../examples/jest), [TypeScript](../examples/typescript) (with project + references) +- **Settings are one-offs.** These are typically for applications or tools that require their own + config, but aren't prevalent throughout the entire repository. + - Examples: [Astro](../examples/astro), [Next](../examples/next), [Nuxt](../examples/nuxt), + [Remix](../examples/remix), Tailwind + +## Dependency management + +Dependencies, also known as node modules, are required by all projects, and are installed through a +package manager like npm, pnpm, or yarn. It doesn't matter which package manager you choose, but we +highly suggest choosing one that has proper workspaces support. If you're unfamiliar with +workspaces, they will: + +- Resolve all `package.json`'s in a repository using glob patterns. +- Install dependencies from all `package.json`'s at once, in the required locations. +- Create symlinks of local packages in `node_modules` (to emulate an installed package). +- Deduplicate and hoist `node_modules` when applicable. + +All of this functionality enables robust monorepo support, and can be enabled with the following: + +import PackageWorkspaces from '../../__partials__/node/package-workspaces.mdx'; + + + +:::caution + +Package workspaces are not a requirement for monorepos, but they do solve an array of problems +around module resolution, avoiding duplicate packages in bundles, and general interoperability. +Proceed with caution for non-workspaces setups! + +::: + +### Workspace commands + +The following common commands can be used for adding, removing, or managing dependencies in a +workspace. View the package manager's official documentation for a thorough list of commands. + +import WsCommands from './__partials__/workspace-commands.mdx'; + + + +### Developer tools at the root + +While not a strict guideline to follow, we've found that installing universal developer tool related +dependencies (Babel, ESLint, Jest, TypeScript, etc) in the root `package.json` as `devDependencies` +to be a good pattern for consistency, quality, and the health of the repository. It provides the +following benefits: + +- It ensures all projects are utilizing the same version (and sometimes configuration) of a tool. +- It allows the tool to easily be upgraded. Upgrade once, applied everywhere. +- It avoids conflicting or outdated versions of the same package. + +With that being said, this _does not_ include development dependencies that are unique to a project! + +### Product libraries in a project + +Product, application, and or framework specific packages should be installed as production +`dependencies` in a project's `package.json`. We've found this pattern to work well for the +following reasons: + +- Application dependencies are pinned per project, avoiding accidental regressions. +- Applications can upgrade their dependencies and avoid breaking neighbor applications. + +## Code sharing + +One of the primary reasons to use a monorepo is to easily share code between projects. When code is +co-located within the same repository, it avoids the overhead of the "build -> version -> publish to +registry -> upgrade in consumer" workflow (when the code is located in an external repository). + +Co-locating code also provides the benefit of fast iteration, fast adoption, and easier migration +(when making breaking changes for example). + +With [package workspaces](#dependency-management), code sharing is a breeze. As mentioned above, +every project that contains a `package.json` that is part of the workspace, will be symlinked into +`node_modules`. Because of this, these packages can easily be imported using their `package.json` +name. + +```ts +// Imports from /packages/utils/package.json +import utils from '@company/utils'; +``` + +### Depending on packages + +Because packages are symlinked into `node_modules`, we can depend on them as if they were normal npm +packages, but with 1 key difference. Since these packages aren't published, they do not have a +version to reference, and instead, we can use the special `workspace:^` version (yarn and pnpm only, +use `*` for npm). + +```json +{ + "name": "@company/consumer", + "dependencies": { + "@company/provider": "workspace:^" + } +} +``` + +The `workspace:` version basically means "use the package found in the current workspace". The `:^` +determines the version range to _substitute with when publishing_. For example, the `workspace:^` +above would be replaced with version of `@company/provider` as `^` when the +`@company/consumer` package is published. + +There's also `workspace:~` and `workspace:*` which substitutes to `~` and `` +respectively. We suggest using `:^` so that version ranges can be deduped. + +### Types of packages + +When sharing packages in a monorepo, there's typically 3 different kinds of packages: + +#### Local only + +A local only package is just that, it's only available locally to the repository and _is not_ +published to a registry, and _is not_ available to external repositories. For teams and companies +that utilize a single repository, this will be the most common type of package. + +A benefit of local packages is that they do not require a build step, as source files can be +imported directly ([when configured correctly](#bundler-integration)). This avoids a lot of +`package.json` overhead, especially in regards to `exports`, `imports`, and other import patterns. + +#### Internally published + +An internal package is published to a private registry, and _is not_ available to the public. +Published packages are far more strict than local packages, as the `package.json` structure plays a +much larger role for downstream consumers, as it dictates how files are imported, where they can be +found, what type of formats are supported (CJS, ESM), so on and so forth. + +Published packages require a build step, for both source code and TypeScript types (when +applicable). We suggest using [esbuild](https://esbuild.github.io/) or +[Packemon](../examples/packemon) to handle this entire flow. With that being said, local projects +can still [import their source files](#bundler-integration). + +#### Externally published + +An external package is structured similarly to an internal package, but instead of publishing to a +private registry, it's published to the npm public registry. + +External packages are primarily for open source projects, and require the repository to also be +public. + +### Bundler integration + +Co-locating packages is great, but how do you import and use them effectively? The easiest solution +is to configure resolver aliases within your bundler (Webpack, Vite, etc). By doing so, you enable +the following functionality: + +- Avoids having to build (and rebuild) the package everytime its code changes. +- Enables file system watching of the package, not just the application. +- Allows for hot module reloading (HMR) to work. +- Package code is transpiled and bundled alongside application code. + + + + +```ts title="vite.config.ts" +import path from 'path'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + // ... + resolve: { + alias: { + '@company/utils': path.join(__dirname, '../packages/utils/src'), + }, + }, +}); +``` + + + + +```ts title="webpack.config.js" +const path = require('path'); + +module.exports = { + // ... + resolve: { + alias: { + '@company/utils': path.join(__dirname, '../packages/utils/src'), + }, + }, +}; +``` + + + + +:::info + +When configuring aliases, we suggest using the `package.json` name as the alias! This ensures that +on the consuming side, you're using the package as if it's a normal node module, and avoids +deviating from the ecosystem. + +::: + +### TypeScript integration + +We suggest using TypeScript project references. Luckily, we have an +[in-depth guide on how to properly and efficiently integrate them](./typescript-project-refs)! diff --git a/website/docs/setup-toolchain.mdx b/website/docs/setup-toolchain.mdx index 343ac4bc2e9..601ca5e6a0c 100644 --- a/website/docs/setup-toolchain.mdx +++ b/website/docs/setup-toolchain.mdx @@ -37,69 +37,9 @@ moon was built with monorepos in mind, and as such, has first-class support for workspaces (but is not a requirement). To utilize workspaces, enable them for your chosen package manager. - - - -```json title="package.json" -{ - // ... - "workspaces": ["apps/*", "packages/*"] -} -``` - -```yaml title=".yarnrc.yml" -# ... -nodeLinker: 'node-modules' -``` - -- [Documentation](https://yarnpkg.com/features/workspaces) - - - - -```json title="package.json" -{ - // ... - "workspaces": ["apps/*", "packages/*"] -} -``` - -- [Documentation](https://classic.yarnpkg.com/en/docs/workspaces) - - - - -```json title="package.json" -{ - // ... - "workspaces": ["apps/*", "packages/*"] -} -``` - -- [Documentation](https://docs.npmjs.com/cli/v8/using-npm/workspaces) - - - - -```yaml title="pnpm-workspace.yaml" -packages: - - 'apps/*' - - 'packages/*' -``` - -- [Documentation](https://pnpm.io/workspaces) +import PackageWorkspaces from './__partials__/node/package-workspaces.mdx'; - - + ## Configuring Node.js diff --git a/website/sidebars.js b/website/sidebars.js index 4e54377a3bf..f41514f6a21 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -169,6 +169,7 @@ const sidebars = { label: 'JavaScript', collapsed: false, items: [ + 'guides/javascript/node-handbook', 'guides/javascript/typescript-project-refs', { type: 'category',