Packages and public modules

How a package publishes a module in a project created from the beyond-web template, how to add a public module and a sibling package whose bare import is preserved, and how to check each change.

  • Availability: Experimental
  • Evidence: Recorded run
  • How-to guide

Before you start

You need a project created from the beyond-web template 0.1.0 and its development server running: see The project template. The words used here are defined in Develop with Beyond.

How a package publishes a module

JSONpackage.json
{
	"name": "@project/app",
	"version": "0.1.0",
	"private": true,
	"description": "The web application of this project",
	"exports": {
		"./main": "./main/index.ts"
	},
	"dependencies": {},
	"beyond": {
		"modules": ".",
		"bundler": "ts"
	},
	"bundlers": {
		"ts": "@beyond-js/packages/bundlers/ts"
	}
}
Member Meaning
name, version The package and its exact version. Module addresses always carry this version, so when you change it the address changes with it
exports The public modules. An entry whose target is a source entry point publishes a module: "./main": "./main/index.ts" publishes @project/app/main. What that entry file exports, and only that, is the public API of the module
dependencies Every package that this one imports by bare specifier. A module of the same package needs no declaration
beyond.modules The directory of the package in which module.json files are looked for. . is the package itself
beyond.bundler The default bundler of the package, applied to every module that does not select one: ts
bundlers The registry of bundlers: where each bundler name comes from. ts is @beyond-js/packages/bundlers/ts

A module.json beside the entry point is optional and adds the specification of the module. In the template it declares where the module runs:

JSONmodule.json
{
	"platforms": ["web"]
}

platforms is ["web"] for browsers and ["node", "web"] for both. A module requested for a platform it does not declare fails with CONDITIONAL_NOT_FOUND.

Package, bundler and processor authors

For Beyond authoring with TypeScript and module-specific features, start with module.json: it declares the module and configures compilation. You do not need a duplicate package exports entry. Helpers inside the module stay private. Package exports are an alternative declaration route associated with standard JavaScript packaging; source TypeScript targets are also accepted. The template combines both forms, but that is not mandatory.

In the current implementation, beyond.modules selects where manifests are discovered. A manifest derives its subpath from its directory or supplies subpath; bundler selects a registered implementation, falling back to beyond.bundler. A manifest-only module using the current ts bundler supplies entry, relative to its directory, such as index.ts. Its public identity is the package name plus the subpath. This manifest-only path is source-observed here, not an additional executed tutorial.

Compiled artifacts and their import map are separate from authoring declarations. The distribution writer lists outputs in beyond-distribution.json; it does not automatically rewrite package exports.

A bundler author can implement the core module/conditional contract directly or use the optional @beyond-js/packages/sdk to compose reusable processors. A processor author implements transformations used by those bundlers. Neither authoring a package nor implementing a bundler requires a separate packager layer.

Add a public module using the template’s combined declarations

  1. Create a directory in the package, for example packages/app/settings/, with an index.ts entry point and a module.json such as { "platforms": ["web"] }.
  2. Add it to the exports of packages/app/package.json: "./settings": "./settings/index.ts".
  3. Export its public API from index.ts. Keep helpers in other files of the directory and import them with relative paths.
  4. Import it from other modules as @project/app/settings.

Add a sibling package with a preserved bare import

This is the way to share code between packages. The example adds @project/shared with a text module that the application imports.

  1. Create packages/shared/package.json:
JSONpackage.json
{
	"name": "@project/shared",
	"version": "0.1.0",
	"private": true,
	"exports": { "./text": "./text/index.ts" },
	"dependencies": {},
	"beyond": { "modules": ".", "bundler": "ts" },
	"bundlers": { "ts": "@beyond-js/packages/bundlers/ts" }
}
  1. Create packages/shared/text/module.json with { "platforms": ["web"] }, and the entry point packages/shared/text/index.ts:
TypeScriptindex.ts
export const greeting = (name: string): string => `Hello from shared, ${name}`;
  1. Register the package in beyond.json:
JSONbeyond.json
{
	"packages": ["packages/app", "packages/shared"]
}
  1. Declare the dependency in packages/app/package.json. The version of the workspace package must satisfy the range you declare:
JSONpackage.json
{
	"name": "@project/app",
	"version": "0.1.0",
	"private": true,
	"description": "The web application of this project",
	"exports": {
		"./main": "./main/index.ts"
	},
	"dependencies": {
		"@project/shared": "0.1.0"
	},
	"beyond": {
		"modules": ".",
		"bundler": "ts"
	},
	"bundlers": {
		"ts": "@beyond-js/packages/bundlers/ts"
	}
}
  1. Import it by its bare specifier, for example in packages/app/main/texts.ts:
TypeScripttexts.ts
import { greeting } from '@project/shared/text';

/**
 * What the element says. The title now comes from a public module of another package, imported by its
 * bare specifier.
 */
export const texts = {
	title: greeting('Beyond'),
	description: 'This page is the public module @project/app/main, compiled and served by Beyond Packages.',
	action: 'Count',
	count: (value: number): string => (value === 1 ? '1 click' : `${value} clicks`)
};

Manifests (beyond.json, package.json, module.json) are read again on the next request to the development server. You do not restart it.

Expected outcome: <endpoint>/preview/entry.json lists @project/shared/text with "source": "environment", and the compiled application module still contains from '@project/shared/text'. The two modules are two artifacts; the source of one is not copied into the other.

Check a change

Each check proves one thing only. A successful compilation does not prove rendering.

Shellcheck.sh
# <endpoint> is the address the development server printed when it started.

# It compiles, and every module has an address: "diagnostics" is []
curl -s <endpoint>/preview/entry.json

# Every public module of the workspace builds: each one is "valid", or lists its diagnostics
curl -s <endpoint>/state

# The boundary is preserved: the compiled module keeps from '<bare specifier>' for other public modules
curl -s "<endpoint>/m/@project/[email protected]/modules/main?target=browser&format=esm&env=development&min=false&sourcemap=none&types=false&css=false"

Then load <endpoint>/preview/ in a browser to check rendering, styles and behavior. After an edit, load it again: the running page is not updated in place.

Rules that keep the boundary

  • Never import across a module boundary with a relative path. Use the public specifier.
  • Never add an exports entry for an internal file only to make an import work. Decide first whether it is public API.
  • The template's ts bundler selects .ts/.tsx sources and skips .d.ts runtime emission. Keep tests outside those inputs until test exclusion is supported; source selection depends on the bundler.
  • Do not name an internal directory module: ./module would resolve to the module.json manifest.
  • Browser modules must not import Node built-ins.
  • Do not write into .beyond/ and do not commit it. It holds the development selection, which changes only when someone asks for it.

When the build refuses

Code Meaning What to do
DEPENDENCY_NOT_DECLARED A package is imported by bare specifier and is not in the dependencies of the importing package Declare it
DEPENDENCY_INCOMPATIBLE The version of the workspace package does not satisfy the declared range Correct the range or the version
CONDITIONAL_NOT_FOUND The module was requested for a platform its module.json does not declare Add the platform to platforms, if the module really runs there
PREVIEW_BUILTIN A browser module imports a Node built-in Remove the import
PREVIEW_CDN_UNSET The server was started without BEYOND_CDN_ORIGIN Start it again with the variable set. Do not work around it
PREVIEW_VERSION_UNRESOLVED A package outside the workspace has no exact installed or declared version Declare the exact version

Limits

  • A workspace package that is not selected for development is requested from the CDN at the version in its package.json, so it must have been published there. Until somebody selects, every package of the workspace is in development.
  • The template, version 0.1.0, declares no widget and no stylesheet: its element is built from web standards, see the element and its styles. The bundler it selects does compile both: see Author a Beyond widget and Styles.
  • Publishing a package and releasing an application are separate operations with their own tools and permissions, and the project has no command for either. The CDN guides describe publishing Beyond source to npm.

Next action

See how the compiled module is addressed over HTTP: URLs and identities.