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
{
"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:
{
"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
- Create a directory in the package, for example
packages/app/settings/, with anindex.tsentry point and amodule.jsonsuch as{ "platforms": ["web"] }. - Add it to the
exportsofpackages/app/package.json:"./settings": "./settings/index.ts". - Export its public API from
index.ts. Keep helpers in other files of the directory and import them with relative paths. - 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.
- Create
packages/shared/package.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" }
}- Create
packages/shared/text/module.jsonwith{ "platforms": ["web"] }, and the entry pointpackages/shared/text/index.ts:
export const greeting = (name: string): string => `Hello from shared, ${name}`;- Register the package in
beyond.json:
{
"packages": ["packages/app", "packages/shared"]
}- Declare the dependency in
packages/app/package.json. Theversionof the workspace package must satisfy the range you declare:
{
"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"
}
}- Import it by its bare specifier, for example in
packages/app/main/texts.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.
# <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/app@0.1.0/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
exportsentry for an internal file only to make an import work. Decide first whether it is public API. - The template's
tsbundler selects.ts/.tsxsources and skips.d.tsruntime emission. Keep tests outside those inputs until test exclusion is supported; source selection depends on the bundler. - Do not name an internal directory
module:./modulewould resolve to themodule.jsonmanifest. - 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.