Skip to content

Commit

Permalink
New Integration: Server-Only Modules (#67)
Browse files Browse the repository at this point in the history
* implementation

* add test

* update lockfile

* add changeset

* add docs

* fail on ts, mts, cts, tsx, js, mjs, cjs, jsx

* switch emoji
  • Loading branch information
lilnasy committed Jan 11, 2024
1 parent 0669768 commit b48b367
Show file tree
Hide file tree
Showing 12 changed files with 209 additions and 0 deletions.
5 changes: 5 additions & 0 deletions .changeset/light-goats-bathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"astro-server-only-modules": major
---

Initial release
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ A collection of purpose-built, small, third-party integrations for Astro.
- **astro-hono** - run your app on a Hono-powered server. [README](https://github.com/lilnasy/gratelets/tree/main/packages/hono) | [NPM](https://www.npmjs.com/package/astro-hono)
- **astro-prerender-patterns** - control rendering modes for all pages and endpoints right from the configuration. [README](https://github.com/lilnasy/gratelets/tree/main/packages/prerender-patterns) | [NPM](https://www.npmjs.com/package/astro-prerender-patterns)
- **astro-scope** - get the hash used by the astro compiler to scope css rules. [README](https://github.com/lilnasy/gratelets/tree/main/packages/scope) | [NPM](https://www.npmjs.com/package/astro-scope)
- **astro-server-only-modules** - prevent certain modules from being imported into client-side code. [README](https://github.com/lilnasy/gratelets/tree/main/packages/server-only-modules) | [NPM](https://www.npmjs.com/package/astro-server-only-modules)
- **astro-stylex** - CSS-in-Zero-JS using [StyleX](https://stylexjs.com/). [README](https://github.com/lilnasy/gratelets/tree/main/packages/stylex) | [NPM](https://www.npmjs.com/package/astro-stylex)
- **astro-typed-api** - type-safe API routes. [README](https://github.com/lilnasy/gratelets/tree/main/packages/typed-api) | [NPM](https://www.npmjs.com/package/astro-typed-api)
58 changes: 58 additions & 0 deletions packages/server-only-modules/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# astro-server-only-modules 🔐

This **[Astro integration][astro-integration]** allows you to make sure you never leak security-sensitive code to the browser.

- <strong>[Why astro-server-only-modules?](#why-astro-server-only-modules)</strong>
- <strong>[Installation](#installation)</strong>
- <strong>[Usage](#usage)</strong>
- <strong>[Troubleshooting](#troubleshooting)</strong>
- <strong>[Contributing](#contributing)</strong>
- <strong>[Changelog](#changelog)</strong>

## Why astro-server-only-modules?

In a large codebase, it can be difficult to keep track of how code is being shared and where. This becomes a security risk when you have critical information that should only be available to the server. There are parts of your infrastructure that the browser (and therefore, a malicious user) does not need to be privy to.

This integration allows you to delineate the context of your code to only include it in the server app. If one of the modules ending with `.server.ts` extension accidentally gets imported by client-side, directly or indirectly, the build will fail.

## Installation

### Manual Install

First, install the `astro-server-only-modules` package using your package manager. If you're using npm or aren't sure, run this in the terminal:

```sh
npm install astro-server-only-modules
```

Then, apply this integration to your `astro.config.*` file using the `integrations` property:

```diff lang="js" "serverOnlyModules()"
// astro.config.mjs
import { defineConfig } from 'astro/config';
+ import serverOnlyModules from 'astro-server-only-modules';

export default defineConfig({
// ...
+ integrations: [serverOnlyModules()],
// ^^^^^^^^^^^^^^^^^
});
```

## Usage

Once the integration is installed and added to the configuration file, rename modules that should only be used within the server to end with `.server.ts`. If one of these modules accidentally gets imported by client-side, directly or indirectly, the build will fail.

## Troubleshooting

For help, check out the `Discussions` tab on the [GitHub repo](https://github.com/lilnasy/gratelets/discussions).

## Contributing

This package is maintained by [lilnasy](https://github.com/lilnasy) independently from Astro. The integration code is located at [packages/server-only-modules/integration.ts](https://github.com/lilnasy/gratelets/blob/main/packages/server-only-modules/integration.ts). You're welcome to contribute by submitting an issue or opening a PR!

## Changelog

See [CHANGELOG.md](https://github.com/lilnasy/gratelets/blob/main/packages/server-only-modules/CHANGELOG.md) for a history of changes to this integration.

[astro-integration]: https://docs.astro.build/en/guides/integrations-guide/
46 changes: 46 additions & 0 deletions packages/server-only-modules/integration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { AstroIntegration } from "astro"

interface Options {}

/**
* Prevents modules with the extension `.server.ts` from being imported into client-side code.
*/
export default function (_?: Options): AstroIntegration {
let buildingFor: "server" | "client" | undefined = undefined
return {
name: "server-only-modules",
hooks: {
"astro:config:setup" ({ updateConfig }) {
updateConfig({ vite: { plugins: [{
name: "server-only-modules/vite",
load(specifier) {
if (buildingFor === "client") {
if (
specifier.endsWith(".server.ts") ||
specifier.endsWith(".server.mts") ||
specifier.endsWith(".server.cts") ||
specifier.endsWith(".server.tsx") ||
specifier.endsWith(".server.js") ||
specifier.endsWith(".server.mjs") ||
specifier.endsWith(".server.cjs") ||
specifier.endsWith(".server.jsx")
) {
throw new ServerOnlyModule
}
}
},
}] } })
},
"astro:build:setup" ({ target }) {
buildingFor = target
}
}
}
}

class ServerOnlyModule extends Error {
name = "ServerOnlyModule"
constructor() {
super(`Cannot import a server-only module in the client build.`)
}
}
31 changes: 31 additions & 0 deletions packages/server-only-modules/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"name": "astro-server-only-modules",
"version": "0.0.0",
"description": "Make sure you never leak security-sensitive code to the browser.",
"author": "Arsh",
"license": "Public Domain",
"keywords": [
"withastro",
"astro-component"
],
"homepage": "https://github.com/lilnasy/gratelets/tree/main/packages/server-only-modules",
"repository": {
"type": "git",
"url": "https://github.com/lilnasy/gratelets",
"directory": "packages/server-only-modules"
},
"files": [
"integration.ts"
],
"exports": {
".": "./integration.ts"
},
"scripts": {
"test": "pnpm -w test server-only-modules.test.ts"
},
"type": "module",
"devDependencies": {
"@types/node": "20",
"astro": "4"
}
}
18 changes: 18 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions tests/fixtures/adds-to-head/src/pages/index.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { getEntryBySlug } from "astro:content"
import IndirectRenderer from "../components/IndirectRenderer.astro"
const entry = await getEntryBySlug("blog", "promo/launch-week-component")
entry.Component;
entry.addsToHead = true;
---
<html>
<head>
Expand Down
7 changes: 7 additions & 0 deletions tests/fixtures/server-only-modules/astro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineConfig } from "astro/config"
import serverOnlyModules from "astro-server-only-modules"

// https://astro.build/config
export default defineConfig({
integrations: process.env.INCLUDE === "true" ? [serverOnlyModules()] : [],
});
8 changes: 8 additions & 0 deletions tests/fixtures/server-only-modules/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"name": "@test/server-only-modules",
"private": true,
"dependencies": {
"astro": "4",
"astro-server-only-modules": "workspace:*"
}
}
16 changes: 16 additions & 0 deletions tests/fixtures/server-only-modules/src/pages/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width" />
<meta name="generator" content={Astro.generator} />
<title>Astro</title>
<script>
import { X } from "../x.server"
console.log(X)
</script>
</head>
<body>
<h1>Astro</h1>
</body>
</html>
1 change: 1 addition & 0 deletions tests/fixtures/server-only-modules/src/x.server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const X = "X"
16 changes: 16 additions & 0 deletions tests/server-only-modules.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { describe, expect, test } from "vitest"
import { build } from "./utils.ts"

describe("astro-server-only-modules", () => {
test("Build succeeds without the integration.", async () => {
process.env.INCLUDE = "false"
await build("./fixtures/server-only-modules")
})
test("Build fails with the integration.", async () => {
process.env.INCLUDE = "true"
try {
await build("./fixtures/server-only-modules")
expect.unreachable()
} catch {}
})
})

0 comments on commit b48b367

Please sign in to comment.