Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

create deno workspace #4

Open
hemedani opened this issue Oct 8, 2024 · 3 comments
Open

create deno workspace #4

hemedani opened this issue Oct 8, 2024 · 3 comments
Assignees
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed

Comments

@hemedani
Copy link
Member

hemedani commented Oct 8, 2024

To set up a Deno workspace for your project, you'll want to structure it in a way that allows modularity, ease of development, and scalability. Here's a step-by-step guide to creating a workspace in Deno:

1. Workspace Overview

In Deno, a workspace setup helps you manage multiple projects (or packages/modules) under a single repository. This is ideal for separating concerns such as backend, frontend, shared libraries, and utilities while keeping everything unified.

A basic structure might look like this:

my-deno-workspace/
├── backend/
│   ├── main.ts
│   ├── deps.ts
│   └── tests/
├── frontend/
│   ├── components/
│   ├── routes/
│   └── main.ts
├── shared/
│   ├── utils.ts
│   └── types.ts
├── .github/
│   └── workflows/
├── README.md
├── deno.json
└── deno.lock

2. Setting Up the Workspace

2.1. Root Directory

Create a root directory for your workspace, which will contain the backend, frontend, and shared code. This directory will also include the configuration files (deno.json, deno.lock).

2.2. Create a deno.json File

In the root of your workspace, create a deno.json file to manage dependencies, import maps, and compiler options for the entire project. This file allows you to configure how Deno resolves imports across the workspace.

{
  "importMap": "./import_map.json",
  "tasks": {
    "start:backend": "deno run --allow-net --allow-read backend/main.ts",
    "start:frontend": "deno run --allow-net --allow-read frontend/main.ts",
    "test": "deno test --coverage"
  },
  "compilerOptions": {
    "lib": ["dom", "deno.ns"],
    "strict": true
  }
}

2.3. Create an import_map.json

To avoid relative paths for shared code and dependencies, create an import_map.json file to centralize import paths.

{
  "imports": {
    "http/": "https://deno.land/std@0.205.0/http/",
    "shared/": "./shared/"
  }
}

2.4. Backend Directory

In the backend/ directory:

  • main.ts: This file will be the entry point for the backend service. It should start the server and handle routing for APIs.
  • deps.ts: This file manages the backend dependencies (e.g., third-party modules).

Example main.ts for a simple server:

import { serve } from "http/server.ts";

serve(() => new Response("Hello from the backend!"), { port: 8000 });

2.5. Frontend Directory

In the frontend/ directory:

  • main.ts: Entry point for the Deno Fresh-based frontend.
  • components/ and routes/ folders for Fresh components and routes.

Example main.ts:

import { start } from "$fresh/server.ts";
import manifest from "./fresh.gen.ts";

await start(manifest);

2.6. Shared Directory

The shared/ folder is where you place reusable modules, such as utility functions, types, or constants that both the backend and frontend can use.

  • utils.ts: For shared utility functions.
  • types.ts: For TypeScript types or interfaces shared across the workspace.

2.7. GitHub Actions for CI/CD

Set up GitHub Actions for CI/CD in .github/workflows/. You might want to automate tasks such as running tests, building the project, and deploying.

Example CI workflow (ci.yml):

name: CI

on:
  push:
    branches:
      - main
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v2
    - name: Setup Deno
      uses: denoland/setup-deno@v1
      with:
        deno-version: v1.x
    - run: deno task test

2.8. README.md

Create a README.md that documents the structure of your workspace, how to set up the environment, and how to run both the backend and frontend.

3. Running and Testing the Workspace

With the workspace set up, you can start different services via the deno.json task system:

  • Start the backend: deno task start:backend
  • Start the frontend: deno task start:frontend
  • Run tests: deno task test

4. Benefits of Deno Workspace

  • Modularity: Each part (backend, frontend, shared) is cleanly separated but can easily share code.
  • Centralized Configurations: Manage dependencies, imports, and tasks from a single source using deno.json.
  • Reusability: Shared code between frontend and backend, reducing code duplication.

This setup allows for flexibility and organization, making it easier to manage and contribute to your open-source project.

@hemedani hemedani self-assigned this Oct 8, 2024
@hemedani hemedani converted this from a draft issue Oct 8, 2024
@hemedani hemedani added this to the clean boilerplate milestone Oct 8, 2024
@hemedani hemedani added enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed labels Oct 8, 2024
@Syd-Amir
Copy link
Collaborator

Syd-Amir commented Oct 8, 2024

I agree with that.
we can also add Lesan declaration folder at the root level and used it in every project.

@Syd-Amir
Copy link
Collaborator

Syd-Amir commented Oct 8, 2024

In new version of deno we can create right and more powerful workspace exactly inside deno.json file:

Deno supports workspaces, also known as "monorepos", which allow you to manage
multiple related and interdependent packages simultaneously.

A "workspace" is a collection of folders containing deno.json or
package.json configuration files. The root deno.json file defines the
workspace:

{
  "workspace": ["./add", "./subtract"]
}

This configures a workspace with add and subtract members, which are
directories expected to have deno.json(c) and/or package.json files.

:::info Naming

Deno uses workspace rather than npm's workspaces to represent a singular
workspace with multiple members.

:::

Example

Let's expand on the deno.json workspace example and see its functionality. The
file hierarchy looks like this:

/
├── deno.json
├── main.ts
├── add/
│     ├── deno.json
│     └── mod.ts
└── subtract/
      ├── deno.json
      └── mod.ts

There are two workspace members (add and subtract), each with mod.ts files.
There is also a root deno.json and a main.ts.

The top-level deno.json configuration file defines the workspace and a
top-level import map applied to all members:

{
  "workspace": ["./add", "./subtract"],
  "imports": {
    "chalk": "npm:chalk@5"
  }
}

The root main.ts file uses the chalk bare specifier from the import map and
imports the add and subtract functions from the workspace members. Note that
it imports them using @scope/add and @scope/subtract, even though these are
not proper URLs and aren't in the import map. How are they resolved?

import chalk from "chalk";
import { add } from "@scope/add";
import { subtract } from "@scope/subtract";

console.log("1 + 2 =", chalk.green(add(1, 2)));
console.log("2 - 4 =", chalk.red(subtract(2, 4)));

In the add/ subdirectory, we define a deno.json with a "name" field, which
is important for referencing the workspace member. The deno.json file also
contains example configurations, like turning off semicolons when using
deno fmt.

{
  "name": "@scope/add",
  "version": "0.1.0",
  "exports": "./mod.ts",
  "fmt": {
    "semiColons": false
  }
}
export function add(a: number, b: number): number {
  return a + b;
}

The subtract/ subdirectory is similar but does not have the same deno fmt
configuration.

{
  "name": "@scope/subtract",
  "version": "0.3.0",
  "exports": "./mod.ts"
}
import { add } from "@scope/add";

export function subtract(a: number, b: number): number {
  return add(a, b * -1);
}

Let's run it:

> deno run main.ts
1 + 2 = 3
2 - 4 = -2

There's a lot to unpack here, showcasing some of the Deno workspace features:

  1. This monorepo consists of two packages, placed in ./add and ./subtract
    directories.

  2. By using name and version options in members' deno.json files, it's
    possible to refer to them using "bare specifiers" across the whole workspace.
    In this case, the packages are named @scope/add and @scope/subtract,
    where scope is the "scope" name you can choose. With these two options,
    it's not necessary to use long and relative file paths in import statements.

  3. npm:chalk@5 package is a shared dependency in the entire workspace.
    Workspace members "inherit" imports of the workspace root, allowing to
    easily manage a single version of a dependency across the codebase.

  4. add subdirectory specifies in its deno.json that deno fmt should not
    apply semicolons when formatting the code. This makes for a much smoother
    transition for existing projects, without a need to change tens or hundreds
    of files in one go.


Deno workspaces are flexible and can work with Node packages. To make migration
for existing Node.js projects easier you can have both Deno-first and Node-first
packages in a single workspace.

Migrating from npm workspaces

Deno workspaces support using a Deno-first package from an existing npm package.
In this example, we mix and match a Deno library called @deno/hi, with a
Node.js library called @deno/log that we developed a couple years back.

We'll need to include a deno.json configuration file in the root:

{
  "workspace": {
    "members": ["hi"]
  }
}

Alongside our existing package.json workspace:

{
  "workspaces": ["log"]
}

The workspace currently has a log npm package:

{
  "name": "@deno/log",
  "version": "0.5.0",
  "type": "module",
  "main": "index.js"
}
export function log(output) {
  console.log(output);
}

Let's create an @deno/hi Deno-first package that imports @deno/log:

{
  "name": "@deno/hi",
  "version": "0.2.0",
  "exports": "./mod.ts",
  "imports": {
    "log": "npm:@deno/log@^0.5"
  }
}
import { log } from "log";

export function sayHiTo(name: string) {
  log(`Hi, ${name}!`);
}

Now, we can write a main.ts file that imports and calls hi:

import { sayHiTo } from "@deno/hi";

sayHiTo("friend");
$ deno run main.ts
Hi, friend!

You can even have both deno.json and package.json in your existing Node.js
package. Additionally, you could remove the package.json in the root and specify
the npm package in the deno.json workspace members. That allows you to gradually
migrate to Deno, without putting a lot of upfront work.

For example, you can add log/deno.json like to to configure Deno's linter and
formatter:

{
  "fmt": {
    "semiColons": false
  },
  "lint": {
    "rules": {
      "exclude": ["no-unused-vars"]
    }
  }
}

Running deno fmt in the workspace, will format the log package to not have
any semicolons, and deno lint won't complain if you leave an unused var in one
of the source files.

Configuring built-in Deno tools

Some configuration options only make sense at the root of the workspace, eg.
specifying nodeModuleDir option in one of the members is not available and
Deno will warn if an option needs to be applied at the workspace root.

Here's a full matrix of various deno.json options available at the workspace
root and its members:

Option Workspace Package Notes
compilerOptions For now we only allow one set of compilerOptions per workspace this because it will require multiple changes to both deno_graph and the TSC integration to allow it. Also we’d have to determine what compilerOptions apply to remote dependencies. We can revisit this in the future.
importMap Exclusive with imports and scopes per config file. It is allowed to have importMap in the workspace config, and imports in the package config.
imports Exclusive with importMap per config file.
scopes Exclusive with importMap per config file.
exclude
lint.include
lint.exclude
lint.files ⚠️ Deprecated
lint.rules.tags Tags are merged by appending package to workspace list. Duplicates are ignored.
lint.rules.include
lint.rules.exclude Rules are merged per package, with package taking priority over workspace (package include is stronger than workspace exclude).
lint.report Only one reporter can be active at a time, so allowing different reporters per workspace would not work in the case where you lint files spanning multiple packages.
fmt.include
fmt.exclude
fmt.files ⚠️ Deprecated
fmt.useTabs Package takes priority over workspace.
fmt.indentWidth Package takes priority over workspace.
fmt.singleQuote Package takes priority over workspace.
fmt.proseWrap Package takes priority over workspace.
fmt.semiColons Package takes priority over workspace.
fmt.options.* ⚠️ Deprecated
nodeModulesDir Resolution behaviour must be the same in the entire workspace.
vendor Resolution behaviour must be the same in the entire workspace.
tasks Package tasks take priority over workspace. cwd used is the cwd of the config file that the task was inside of.
test.include
test.exclude
test.files ⚠️ Deprecated
publish.include
publish.exclude
bench.include
bench.exclude
bench.files ⚠️ Deprecated
lock Only a single lock file may exist per resolver, and only resolver may exist per workspace, so conditional enablement of the lockfile per package does not make sense.
unstable For simplicities sake, we do not allow unstable flags, because a lot of the CLI assumes that unstable flags are immutable and global to the entire process. Also weird interaction with DENO_UNSTABLE_* flags.
name
version
exports
workspace Nested workspaces are not supported.

@hemedani
Copy link
Member Author

hemedani commented Oct 8, 2024

Nice, I try to do this with new docs

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers help wanted Extra attention is needed
Projects
Status: Ready
Development

No branches or pull requests

2 participants