npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@valbuild/next

v0.72.0

Published

Val NextJS: hard-coded content - super-charged

Downloads

790

Readme

🐉 HERE BE DRAGONS 🐉

Val is currently in beta - the API can be considered relatively stable, but expect some features to be broken and the UX to be changing.

Join us on discord to get help or give us feedback.

Table of contents

Installation

  • Make sure your project is using
  • Install the packages:
npm install @valbuild/core@latest @valbuild/next@latest
  • Optionally, but recommend add the eslint-plugin package:
npm install -D  @valbuild/eslint-plugin@latest
  • Run the init script:
npx @valbuild/init@latest

If you do not wish to use the init script, or having issues with it, checkout the manual configuration guide.

Additional setup

  • If you have a monorepo, or have a project where the project is located in a subdirectory relative to the github repository see the monorepos section
  • See formatting published content if you use prettier (or similar) Val to do it as well.
  • If you want editors to update content in production, read up on how to setup remote mode.

Getting started

Create your first Val content file

Content in Val is always defined in .val.ts (or .js) files.

NOTE: the init script will generate an example Val content file (unless you opt out of it).

Val content files are evaluated by Val, therefore they need to abide a set of requirements. If you use the eslint plugins these requirements will be enforced. You can also validate val files using the @valbuild/cli: npx -p @valbuild/cli val validate.

For reference these requirements are:

  • they must export a default content definition (c.define) where the first argument equals the path of the file relative to the val.config file; and
  • they must be declared in the val.modules file; and
  • they must have a default export that is c.define; and
  • they can only import Val related files or types (using import type { MyType } from "./otherModule.ts")

Val content file example

// ./examples/val/example.val.ts
import { s /* s = schema */, c /* c = content */ } from "../../val.config";

/**
 * This is the schema for the content. It defines the structure of the content and the types of each field.
 */
export const schema = s.object({
  /**
   * Basic text field
   */
  text: s.string(),
});

/**
 * This is the content definition. Add your content below.
 *
 * NOTE: the first argument is the path of the file.
 */
export default c.define("/examples/val/example.val.ts", schema, {
  text: "Basic text content",
});

The val.modules file

Once you have created your Val content file, it must be declared in the val.modules.ts (or .js) file in the project root folder.

Example:

import { modules } from "@valbuild/next";
import { config } from "./val.config";

export default modules(config, [
  // Add your modules here
  { def: () => import("./examples/val/example.val") },
]);

Using Val in Client Components

In client components you can access your content with the useVal hook:

// ./app/page.tsx
"use client";
import { useVal } from "../val/val.client";
import exampleVal from "../examples/val/example.val";

export default function Home() {
  const { text } = useVal(exampleVal);
  return <main>{text}</main>;
}

Using Val in React Server Components

In React Server components you can access your content with the fetchVal function:

// ./app/page.tsx
"use server";
import { fetchVal } from "../val/val.rsc";
import exampleVal from "../examples/val/example.val";

export default async function Home() {
  const { text } = await fetchVal(exampleVal);
  return <main>{text}</main>;
}

Remote Mode

Enable remote mode to allow editors to update content online (outside of local development) by creating a project at app.val.build.

NOTE: Your content remains yours. Hosting content from your repository does not require a subscription. However, to edit content online, a subscription is needed — unless your project is a public repository or qualifies for the free tier. Visit the pricing page for details.

WHY: Updating code involves creating a commit, which requires a server. We offer a hosted service for simplicity and efficiency, as self-hosted solutions takes time to setup and maintain. Additionally, the val.build team funds the ongoing development of this library.

Remote Mode Configuration

Once your project is set up in app.val.build, configure your application to use it by setting the following:

Environment Variables

  • VAL_API_KEY: Obtain this from your project's configuration page.
  • VAL_SECRET: Generate a random secret to secure communication between the UX client and your Next.js application.

val.config Properties

Set these properties in the val.config file:

  • project: The fully qualified name of your project, formatted as <team>/<name>.
  • gitBranch: The Git branch your application uses. For Vercel, use VERCEL_GIT_COMMIT_REF.
  • gitCommit: The current Git commit your application is running on. For Vercel, use VERCEL_GIT_COMMIT_SHA.
  • root: Optional. The path to the val.config file. Typically empty or undefined. If the project folder is under web, root would be: /web.

Example val.config.ts

import { initVal } from "@valbuild/next";

const { s, c, val, config } = initVal({
  project: "myteam/myproject",
  //root: "/subdir", // only required for monorepos. Use the path where val.config is located. The path should start with /
  gitBranch: process.env.VERCEL_GIT_COMMIT_REF,
  gitCommit: process.env.VERCEL_GIT_COMMIT_SHA,
});

export type { t } from "@valbuild/next";
export { s, c, val, config };

Formatting published content

If you are using prettier or another code formatting tool, it is recommended to setup formatting of code after changes have been applied.

Setting up formatting using Prettier

  • Install prettier as RUNTIME dependency, by moving the prettier dependency from devDependencies to dependencies. The reason you need to do this, is that Val will be using it at runtime in production, and it has to be part of your build for this to work.

  • Optionally create a .prettierrc.json file unless you have one already. We recommend doing this, so that you can be sure that formatting is applied consistently in both your development environment and by Val. You can set this to be an empty object, if you are want to keep using prettiers defaults:

    {}
  • Add a formatter to the /val/val.server:

    formatter: (code: string, filePath: string) => {
      return prettier.format(code, {
        filepath: filePath,
        ...prettierOptions, // <- use the same rules as in development
      } as prettier.Options);
    },

    Unless you have any modifications in your val.server file, the complete file should now look like this:

    import "server-only";
    import { initValServer } from "@valbuild/next/server";
    import { config } from "../val.config";
    import { draftMode } from "next/headers";
    import valModules from "../val.modules";
    import prettier from "prettier";
    import prettierOptions from "../.prettierrc.json";
    
    const { valNextAppRouter } = initValServer(
      valModules,
      { ...config },
      {
        draftMode,
        formatter: (code: string, filePath: string) => {
          return prettier.format(code, {
            filepath: filePath,
            ...prettierOptions, // <- use the same rules as in development
          } as prettier.Options);
        },
      },
    );
    
    export { valNextAppRouter };

You should now be able to hit the save button locally and see prettier rules being applied.

Other formatters

Val is formatter agnostic, so it is possible to use the same flow as the one described for prettier above to any formatter you might want to use.

NOTE: this will be applied at runtime in production so you need make sure that the formatting dependencies are in the dependencies section of your package.json

Monorepos

Val supports projects that are not under the root path in GitHub, and therefore monorepos. To configure your project for monorepos, you can use the root parameter described in the config section.

Schema types

String

import { s } from "./val.config";

s.string(); // <- Schema<string>

Number

import { s } from "./val.config";

s.number(); // <- Schema<number>

Boolean

import { s } from "./val.config";

s.boolean(); // <- Schema<boolean>

Nullable

All schema types can be nullable (optional). A nullable schema creates a union of the type and null.

import { s } from "./val.config";

s.string().nullable(); // <- Schema<string | null>

Array

s.array(t.string()); // <- Schema<string[]>

Record

The type of s.record is Record.

It is similar to an array, in that editors can add and remove items in it, however it has a unique key which can be used as, for example, the slug or as a part of an route.

NOTE: records can also be used with keyOf.

s.record(t.number()); // <- Schema<Record<string, number>>

Object

s.object({
  myProperty: s.string(),
});

RichText

This means that content will be accessible and according to spec out of the box. The flip-side is that Val will not support RichText that includes elements that is not part of the html 5 standard.

This opinionated approach was chosen since rendering anything, makes it hard for developers to maintain and hard for editors to understand.

RichText Schema

s.richtext({
  // options
});

Initializing RichText content

To initialize some text content using a RichText schema, you can use follow the example below:

import { s, c } from "./val.config";

export const schema = s.richtext({
  // styling
  style: {
    bold: true, // enables bold
    italic: true, // enables italic text
    lineThrough: true, // enables line/strike-through
  },
  // tags:
  block: {
    //ul: true, // enables unordered lists
    //ol: true, // enables ordered lists
    // headings:
    h1: true,
    h2: true,
    // h3: true,
    // h4: true,
    // h5: true,
    // h6: true
  },
  inline: {
    //a: true, // enables links
    //img: true, // enables images
  },
});

export default c.define("/src/app/content", schema, [
  {
    tag: "p",
    children: ["This is richtext"],
  },
  {
    tag: "p",
    children: [{ tag: "span", styles: ["bold"], children: ["Bold"] }, "text"],
  },
]);

Rendering RichText

You can use the ValRichText component to render content.

"use client";
import { ValRichText } from "@valbuild/next";
import contentVal from "./content.val";
import { useVal } from "./val/val.client";

export default function Page() {
  const content = useVal(contentVal);
  return (
    <main>
      <ValRichText
        theme={{
          style: {
            bold: "font-bold", // <- maps bold to a class. NOTE: tailwind classes are supported
          },
          //
        }}
      >
        {content}
      </ValRichText>
    </main>
  );
}

ValRichText: theme property

To add classes to ValRichText you can use the theme property:

<ValRichText
  theme={{
    p: "font-sans",
    // etc
  }}
>
  {content}
</ValRichText>

NOTE: if a theme is defined, you must define a mapping for every tag that the you get. What tags you have is decided based on the options defined on the s.richtext() schema. For example: s.richtext({ style: { bold: true } }) requires that you add a bold theme.

<ValRichText
  theme={{
    h1: "text-4xl font-bold",
    bold: "font-bold",
    img: null, // either a string or null is required
  }}
>
  {content}
</ValRichText>

NOTE: the reason you must define themes for every tag that the RichText is that this will force you to revisit the themes that are used if the schema changes. The alternative would be to accept changes to the schema.

ValRichText: transform property

Vals RichText type maps RichText 1-to-1 with semantic HTML5.

If you want to customize / override the type of elements which are rendered, you can use the transform property.

<ValRichText
  transform={(node, _children, className) => {
    if (typeof node !== "string" && node.tag === "img") {
      return (
        <div className="my-wrapper-class">
          <img {...node} className={className} />
        </div>
      );
    }
    // if transform returns undefined the default render will be used
  }}
>
  {content}
</ValRichText>

The RichText type

The RichText type is actually an AST (abstract syntax tree) representing semantic HTML5 elements.

That means they look something like this:

type RichTextNode = {
  tag:
    | "img"
    | "a"
    | "ul"
    | "ol"
    | "h1"
    | "h2"
    | "h3"
    | "h4"
    | "h5"
    | "h6"
    | "br"
    | "p"
    | "li"
    | "span";
  classes: "bold" | "line-through" | "italic"; // all styling classes
  children: RichTextNode[] | undefined;
};

RichText: full custom

The RichText type maps 1-to-1 to HTML. That means it is straightforward to build your own implementation of a React component that renders RichText.

This example is a simplified version of the ValRichText component. You can use this as a template to create your own.

NOTE: before writing your own, make sure you check out the theme and transform properties on the ValRichText - most simpler cases should be covered by them.

export function ValRichText({
  children: root,
}: {
  children: RichText<MyRichTextOptions>;
}) {
  function build(
    node: RichTextNode<MyRichTextOptions>,
    key?: number,
  ): JSX.Element | string {
    if (typeof node === "string") {
      return node;
    }
    // you can map the classes to something else here
    const className = node.classes.join(" ");
    const tag = node.tag; // one of: "img" | "a" | "ul" | "ol" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "br" | "p" | "li" | "span"

    // Example of rendering img with MyOwnImageComponent:
    if (tag === "img") {
      return <MyOwnImageComponent {...node} />;
    }
    return React.createElement(
      tag,
      {
        key,
        className,
      },
      "children" in node ? node.children.map(build) : null,
    );
  }
  return <div {...val.attrs(root)}>{root.children.map(build)}</div>;
}
type MyRichTextOptions = AnyRichTextOptions; // you can reduce the surface of what you need to render, by restricting the `options` in `s.richtext(options)`

Image

Image Schema

s.image();

Initializing image content

Local images must be stored under the /public/val folder.

import { s, c } from "../val.config";

export const schema = s.image();

export default c.define("/image", schema, c.image("/public/myfile.jpg"));

NOTE: This will not validate, since images requires width, height and mimeType. You can fix validation errors like this by using the CLI or by using the VS Code plugin.

Rendering images

The ValImage component is a wrapper around next/image that accepts a Val Image type.

You can use it like this:

const content = useVal(contentVal); // schema of contentVal: s.object({ image: s.image() })

return <ValImage src={content.image} />;

Using images in components

Images are transformed to object that have a url property which can be used to render them.

Example:

// in a Functional Component
const image = useVal(imageVal);

return <img src={image.url} />;

Union

The union schema can be used to create either "tagged unions" or a union of string literals.

Union Schema tagged unions

A tagged union is a union of objects which all have the same field (of the same type). This field can be used to determine (or "discriminate") the exact type of one of the types of the union.

It is useful when editors should be able to chose from a set of objects that are different.

Example: let us say you have a page that can be one of the following: blog (page) or product (page). In this case your schema could look like this:

s.union(
  "type", // the key of the "discriminator"
  s.object({
    type: s.literal("blogPage"), // <- each type must have a UNIQUE value
    author: s.string(),
    // ...
  }),
  s.object({
    type: s.literal("productPage"),
    sku: s.number(),
    // ...
  }),
); // <- Schema<{ type: "blogPage", author: string } | { type: "productPage", sku: number }>

Union Schema: union of string literals

You can also use a union to create a union of string literals. This is useful if you want a type-safe way to describe a set of valid strings that can be chosen by an editor.

s.union(
  s.literal("one"),
  s.literal("two"),
  //...
); // <- Schema<"one" | "two">

KeyOf

You can use keyOf to reference a key in a record of a Val module.

NOTE: currently you must reference keys in Val modules, you cannot reference keys of values nested inside a Val module. This is a feature on the roadmap.

const schema = s.record(s.object({ nested: s.record(s.string()) }));

export default c.define("/keyof.val.ts", schema, {
  "you-can-reference-me": {
    // <- this can be referenced
    nested: {
      "but-not-me": ":(", // <- this cannot be referenced
    },
  },
});

KeyOf Schema

import otherVal from "./other.val"; // NOTE: this must be a record

s.keyOf(otherVal);

Initializing keyOf

Using keyOf to reference content

const article = useVal(articleVal); // s.object({ author: s.keyOf(otherVal) })
const authors = useVal(otherVal); // s.record(s.object({ name: s.string() }))

const nameOfAuthor = authors[articleVal.author].name;