@hearthlog/tooling 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/hearthlog.mjs +2 -2
- package/package.json +2 -2
- package/src/check/check-layers.ts +3 -2
- package/src/check/check-modules.ts +29 -17
- package/src/check/check-tenant-config.ts +1 -1
- package/src/compose/compose-modules.ts +7 -7
- package/src/compose/module-scaffold.ts +8 -6
- package/src/deploy/deploy-tenant.ts +1 -1
- package/src/generate/build-generated.ts +3 -3
- package/src/generate/generate-tenant.ts +3 -2
- package/src/host/write-host.ts +2 -2
- package/src/root.ts +31 -1
- package/src/tenants/host-configs.ts +1 -1
- package/src/tenants/tenant-config.ts +1 -1
- package/src/vite/config.ts +22 -5
- package/src/vite/vite-plugin-tenant-manifest.ts +2 -1
- package/src/deploy/copy-tenant.ts +0 -395
- package/src/deploy/split-tenant.ts +0 -295
- package/src/tenants/tenant-copy-plan.ts +0 -119
package/bin/hearthlog.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// The `hearthlog` command: runs the TypeScript tools through tsx. `build`, `init`, `export`, `check-module`, `lock`, `artifact`, and
|
|
3
3
|
// `gallery-public:build` start before a site has generated files (or write their own), so they run alone; the
|
|
4
4
|
// `apple:*` commands run the build and then the tools of `@hearthlog/native` (a dependency of the site, not of this
|
|
5
|
-
// package); `ensure`, `deploy`, `deploy-ledger
|
|
5
|
+
// package); `ensure`, `deploy`, and `deploy-ledger` run the build and then a deploy tool; every other
|
|
6
6
|
// command goes to the CLI.
|
|
7
7
|
import { spawnSync } from 'node:child_process';
|
|
8
8
|
import { createRequire } from 'node:module';
|
|
@@ -33,7 +33,7 @@ if (native) {
|
|
|
33
33
|
|
|
34
34
|
// The deploy tools read the site's generated files, so each starts with the build (which does nothing when they are
|
|
35
35
|
// current). They work from a tenant repository because they run from this package, not from the host's scripts.
|
|
36
|
-
const deployTools = { ensure: 'deploy/ensure-tenant.ts', deploy: 'deploy/deploy-tenant.ts', 'deploy-ledger': 'deploy/deploy-ledger.ts'
|
|
36
|
+
const deployTools = { ensure: 'deploy/ensure-tenant.ts', deploy: 'deploy/deploy-tenant.ts', 'deploy-ledger': 'deploy/deploy-ledger.ts' };
|
|
37
37
|
if (deployTools[command]) {
|
|
38
38
|
const site = rest[rest.indexOf('--site') + 1];
|
|
39
39
|
const env = { ...process.env, ...(rest.includes('--site') && site ? { VITE_STOR_TENANT: site } : {}) };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hearthlog/tooling",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"tsx": "^4.23.15",
|
|
31
|
-
"@hearthlog/contracts": "0.1.
|
|
31
|
+
"@hearthlog/contracts": "0.1.5"
|
|
32
32
|
},
|
|
33
33
|
"bin": {
|
|
34
34
|
"hearthlog": "./bin/hearthlog.mjs"
|
|
@@ -12,9 +12,10 @@
|
|
|
12
12
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
13
13
|
import { dirname, join, matchesGlob, relative, resolve, sep } from "node:path";
|
|
14
14
|
import { fileURLToPath } from "node:url";
|
|
15
|
-
import {
|
|
15
|
+
import { PLATFORM_ROOT } from "../root.js";
|
|
16
16
|
|
|
17
|
-
|
|
17
|
+
// Layers are a property of the platform's packages, wherever the check runs from.
|
|
18
|
+
const ROOT = PLATFORM_ROOT;
|
|
18
19
|
const IMPORT = /(?:^|[^\w$.])(?:import|export)\s[^'"]*?from\s+['"]([^'"]+)['"]|import\(\s*['"]([^'"]+)['"]\s*\)|^\s*import\s+['"]([^'"]+)['"]/gm;
|
|
19
20
|
|
|
20
21
|
export interface LayerConfig {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
18
18
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
19
19
|
import { fileURLToPath } from "node:url";
|
|
20
|
-
import { hostRoot } from "../root.js";
|
|
20
|
+
import { hostRoot, PLATFORM_ROOT } from "../root.js";
|
|
21
21
|
|
|
22
22
|
const ROOT = hostRoot();
|
|
23
23
|
const SOURCE = /\.(ts|svelte|js)$/;
|
|
@@ -89,6 +89,15 @@ export async function loadModuleDomain(root = ROOT): Promise<ModuleDomain> {
|
|
|
89
89
|
};
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* The directories a check reads, each with the root it is under: the given ones under the host, or by default the host's
|
|
94
|
+
* own and the platform's (which are the same checkout unless the host is a site beside the platform's packages).
|
|
95
|
+
*/
|
|
96
|
+
function scanned(root: string, platform: string, given: string[] | undefined, host: string[], platformDirectories: string[]): Array<[string, string]> {
|
|
97
|
+
if (given) return given.map((directory) => [root, directory]);
|
|
98
|
+
return [...host.map((directory): [string, string] => [root, directory]), ...platformDirectories.map((directory): [string, string] => [platform, directory])];
|
|
99
|
+
}
|
|
100
|
+
|
|
92
101
|
const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
93
102
|
|
|
94
103
|
/**
|
|
@@ -99,8 +108,10 @@ const escapeRegExp = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\
|
|
|
99
108
|
*/
|
|
100
109
|
export function coreNamesNoDomain(
|
|
101
110
|
root = ROOT,
|
|
102
|
-
directories
|
|
111
|
+
directories?: string[],
|
|
103
112
|
domain: ModuleDomain = { names: [], eventTypes: [] },
|
|
113
|
+
/** The platform checkout, when it is not the host (`directories` are then the host's, under `root`). */
|
|
114
|
+
platform = root,
|
|
104
115
|
): string[] {
|
|
105
116
|
const modulesDirectory = join(root, "modules");
|
|
106
117
|
const featureIds = (existsSync(modulesDirectory) ? readdirSync(modulesDirectory) : []).flatMap((name) => {
|
|
@@ -113,16 +124,16 @@ export function coreNamesNoDomain(
|
|
|
113
124
|
const quoted = quotedNames.length ? new RegExp(`(["'\`])(${quotedNames.map(escapeRegExp).join("|")})\\1`) : null;
|
|
114
125
|
const events = domain.eventTypes.length ? new RegExp(`\\b(${domain.eventTypes.map(escapeRegExp).join("|")})\\b`) : null;
|
|
115
126
|
const problems: string[] = [];
|
|
116
|
-
for (const directory of directories) {
|
|
117
|
-
for (const file of files(join(
|
|
127
|
+
for (const [base, directory] of scanned(root, platform, directories, ["ledger/src"], ["packages/tooling/src/host", "packages/cloudflare/src", "packages/ledger/src", "packages/contracts/src", "packages/runtime/src", "packages/ui/src"])) {
|
|
128
|
+
for (const file of files(join(base, directory))) {
|
|
118
129
|
if (/\.generated\.ts$|\.test\.ts$|\.d\.ts$|[\\/]generated\.ts$|worker-configuration/.test(file) || file.includes(`${sep}generated${sep}`) || file.includes(`${sep}(modules)${sep}`)) continue;
|
|
119
130
|
readFileSync(file, "utf8").split("\n").forEach((line, index) => {
|
|
120
131
|
// Comments may use a domain as an example.
|
|
121
132
|
if (/^\s*(\*|\/\/|\/\*)/.test(line)) return;
|
|
122
133
|
const name = quoted?.exec(line);
|
|
123
|
-
if (name) problems.push(`${relative(
|
|
134
|
+
if (name) problems.push(`${relative(base, file)}:${index + 1}: the core names ${name[1]}${name[2]}${name[1]}`);
|
|
124
135
|
const event = events?.exec(line);
|
|
125
|
-
if (event) problems.push(`${relative(
|
|
136
|
+
if (event) problems.push(`${relative(base, file)}:${index + 1}: the core names the event type ${event[1]}`);
|
|
126
137
|
});
|
|
127
138
|
}
|
|
128
139
|
}
|
|
@@ -135,16 +146,16 @@ export function coreNamesNoDomain(
|
|
|
135
146
|
* to one would break the day the file is no longer in this tree. The SDK facade, which still reaches
|
|
136
147
|
* into `src/lib` by relative path, and the generated files themselves are exempt.
|
|
137
148
|
*/
|
|
138
|
-
export function directGeneratedImports(root = ROOT, directories
|
|
149
|
+
export function directGeneratedImports(root = ROOT, directories?: string[], platform = root): string[] {
|
|
139
150
|
const problems: string[] = [];
|
|
140
151
|
const generatedFile = /(^|[\\/])[\w-]+\.generated(\.js|\.ts)?$|(^|[\\/])tenant[\\/]generated(\.js|\.ts)?$/;
|
|
141
|
-
for (const directory of directories) {
|
|
142
|
-
for (const file of files(join(
|
|
152
|
+
for (const [base, directory] of scanned(root, platform, directories, ["src", "ledger"], ["scripts", "packages/tooling/src", "packages/cloudflare/src", "packages/ledger/src", "packages/contracts/src", "packages/runtime/src", "packages/ui/src"])) {
|
|
153
|
+
for (const file of files(join(base, directory))) {
|
|
143
154
|
// The scripts' own tests write fixture files whose text looks like imports, and the generators emit import lines.
|
|
144
155
|
if (/\.generated\.ts$|[\\/]generated\.ts$/.test(file) || file.includes(`${sep}generated${sep}`) || file.includes(`${sep}(modules)${sep}`) || ((directory === "scripts" || directory === "packages/tooling/src") && /\.test\.ts$|(compose-modules|generate-[a-z-]+)\.ts$/.test(file))) continue;
|
|
145
156
|
for (const specifier of specifiers(file)) {
|
|
146
157
|
if ((specifier.startsWith(".") || specifier.startsWith("#lib")) && generatedFile.test(specifier)) {
|
|
147
|
-
problems.push(`${relative(
|
|
158
|
+
problems.push(`${relative(base, file)}: "${specifier}" is a generated file (import it through #generated/*)`);
|
|
148
159
|
}
|
|
149
160
|
}
|
|
150
161
|
}
|
|
@@ -152,7 +163,7 @@ export function directGeneratedImports(root = ROOT, directories = ["src", "worke
|
|
|
152
163
|
return problems;
|
|
153
164
|
}
|
|
154
165
|
|
|
155
|
-
export function checkBoundaries(root = ROOT, domain?: ModuleDomain): string[] {
|
|
166
|
+
export function checkBoundaries(root = ROOT, domain?: ModuleDomain, platform = root): string[] {
|
|
156
167
|
const problems: string[] = [];
|
|
157
168
|
const modulesDirectory = join(root, "modules");
|
|
158
169
|
// A platform workspace or a tenant repository has no modules directory.
|
|
@@ -174,21 +185,22 @@ export function checkBoundaries(root = ROOT, domain?: ModuleDomain): string[] {
|
|
|
174
185
|
}
|
|
175
186
|
}
|
|
176
187
|
const generated = /\.generated\.ts$|[\\/]\(modules\)[\\/]/;
|
|
177
|
-
for (const directory of ["src", "
|
|
178
|
-
for (const file of files(join(
|
|
188
|
+
for (const [base, directory] of scanned(root, platform, undefined, ["src"], ["packages"])) {
|
|
189
|
+
for (const file of files(join(base, directory))) {
|
|
179
190
|
if (generated.test(file)) continue;
|
|
180
191
|
for (const specifier of specifiers(file)) {
|
|
181
|
-
if (specifier.startsWith("@hearthlog/module-")) problems.push(`${relative(
|
|
192
|
+
if (specifier.startsWith("@hearthlog/module-")) problems.push(`${relative(base, file)}: the core imports a module ("${specifier}")`);
|
|
182
193
|
}
|
|
183
194
|
}
|
|
184
195
|
}
|
|
185
|
-
problems.push(...coreNamesNoDomain(root, undefined, domain));
|
|
186
|
-
problems.push(...directGeneratedImports(root));
|
|
196
|
+
problems.push(...coreNamesNoDomain(root, undefined, domain, platform));
|
|
197
|
+
problems.push(...directGeneratedImports(root, undefined, platform));
|
|
187
198
|
return problems;
|
|
188
199
|
}
|
|
189
200
|
|
|
190
201
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
191
|
-
|
|
202
|
+
// The host (its modules, its own code, the generated catalog) and the platform's packages may be different checkouts.
|
|
203
|
+
const problems = checkBoundaries(ROOT, await loadModuleDomain(ROOT), PLATFORM_ROOT);
|
|
192
204
|
if (problems.length) {
|
|
193
205
|
console.error(`Module boundaries are broken:\n - ${problems.join("\n - ")}`);
|
|
194
206
|
process.exitCode = 1;
|
|
@@ -186,7 +186,7 @@ for (const stack of stackNames) {
|
|
|
186
186
|
if (JSON.stringify(feedSites) !== JSON.stringify(expectedSites)) problems.push(`${label}: the feed Worker should bind ${expectedSites.join(", ")}`);
|
|
187
187
|
// A site on its own stack has a public Gallery Worker only when it ships the Gallery; the shared stack always has one.
|
|
188
188
|
const ownSite = stack === "shared" ? undefined : sites.find((site) => site.id === stack)!;
|
|
189
|
-
if ((!ownSite || ownSite.features.includes("gallery")) && read("gallery-public").name !== siteInfrastructure.galleryPublicWorker) problems.push(`${label}: the public Gallery Worker should be named ${siteInfrastructure.galleryPublicWorker}`);
|
|
189
|
+
if ((!ownSite || (ownSite.features as readonly string[]).includes("gallery")) && read("gallery-public").name !== siteInfrastructure.galleryPublicWorker) problems.push(`${label}: the public Gallery Worker should be named ${siteInfrastructure.galleryPublicWorker}`);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
for (const site of sites) problems.push(...(await moduleLockProblems(site)));
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* gitignored `.generated/` by `pnpm build:generated`, which runs
|
|
13
13
|
* this first because everything else reads it. A site's modules
|
|
14
14
|
* are the ones in its `tenants/<id>/modules.lock.json`.
|
|
15
|
-
* (default) The routes of one tenant (VITE_STOR_TENANT, default "st"):
|
|
15
|
+
* (default) The routes of one tenant (VITE_STOR_TENANT, default the host's only site, else "st"):
|
|
16
16
|
* thin wrappers in the gitignored `src/routes/(modules)/` for
|
|
17
17
|
* the modules whose features the tenant enables, so a tenant
|
|
18
18
|
* without a module ships none of its pages.
|
|
@@ -27,7 +27,7 @@ import { dirname, join, relative, resolve, sep } from "node:path";
|
|
|
27
27
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
28
28
|
import { isDeepStrictEqual } from "node:util";
|
|
29
29
|
import { CORE_ROUTE_PATHS } from "../host/core-routes.js";
|
|
30
|
-
import { hostRoot } from "../root.js";
|
|
30
|
+
import { activeSiteId, hostRoot } from "../root.js";
|
|
31
31
|
|
|
32
32
|
import { findCollisions, validateManifest, type ModuleManifest } from "@hearthlog/contracts";
|
|
33
33
|
|
|
@@ -91,13 +91,13 @@ export function installedModuleDirectories(root = REPOSITORY_ROOT): string[] {
|
|
|
91
91
|
}
|
|
92
92
|
|
|
93
93
|
/**
|
|
94
|
-
* The modules a host has: those in its `modules/` folder (a checkout that develops them) and
|
|
95
|
-
* it installs (a site that uses them). Given a directory, only that folder is scanned.
|
|
94
|
+
* The modules a host has: those in its `modules/` folder (a checkout that develops them, or a link to one) and
|
|
95
|
+
* the packages it installs (a site that uses them). Given a directory, only that folder is scanned.
|
|
96
96
|
*/
|
|
97
97
|
export function loadModules(directory?: string): LoadedModule[] {
|
|
98
98
|
const scanned = (folder: string): string[] => existsSync(folder)
|
|
99
99
|
? readdirSync(folder, { withFileTypes: true })
|
|
100
|
-
.filter((entry) => entry.isDirectory() && existsSync(join(folder, entry.name, "hearthlog.module.json")))
|
|
100
|
+
.filter((entry) => (entry.isDirectory() || entry.isSymbolicLink()) && existsSync(join(folder, entry.name, "hearthlog.module.json")))
|
|
101
101
|
.map((entry) => join(folder, entry.name))
|
|
102
102
|
: [];
|
|
103
103
|
const directories = directory !== undefined ? scanned(directory) : [...scanned(MODULES_DIRECTORY), ...installedModuleDirectories()];
|
|
@@ -448,7 +448,7 @@ export async function generateCatalog({ siteId, all = false, check = false }: Ca
|
|
|
448
448
|
async function composeRoutes(): Promise<void> {
|
|
449
449
|
const modules = loadModules();
|
|
450
450
|
checkComposition(modules);
|
|
451
|
-
const siteId =
|
|
451
|
+
const siteId = activeSiteId();
|
|
452
452
|
const { loadTenantConfig, siteById } = await import("../tenants/tenant-config.js");
|
|
453
453
|
const { sites } = await loadTenantConfig(undefined, { only: siteId });
|
|
454
454
|
const site = siteById(sites, siteId);
|
|
@@ -477,7 +477,7 @@ async function composeRoutes(): Promise<void> {
|
|
|
477
477
|
|
|
478
478
|
export async function main(argv = process.argv.slice(2)): Promise<void> {
|
|
479
479
|
const site = argv.indexOf("--site");
|
|
480
|
-
const siteId = (site >= 0 ? argv[site + 1] : undefined) ?? (
|
|
480
|
+
const siteId = (site >= 0 ? argv[site + 1] : undefined) ?? activeSiteId();
|
|
481
481
|
if (argv.includes("--catalog")) await generateCatalog({ siteId, all: argv.includes("--all"), check: argv.includes("--check") });
|
|
482
482
|
else await composeRoutes();
|
|
483
483
|
}
|
|
@@ -18,6 +18,7 @@ import { fileURLToPath } from "node:url";
|
|
|
18
18
|
|
|
19
19
|
import { MODULE_API_VERSION } from "@hearthlog/contracts";
|
|
20
20
|
|
|
21
|
+
import { PLATFORM_ROOT } from "../root.js";
|
|
21
22
|
import { REPOSITORY_ROOT } from "../tenants/tenant-config.js";
|
|
22
23
|
|
|
23
24
|
const TEMPLATE = join(dirname(fileURLToPath(import.meta.url)), "..", "..", "templates", "module");
|
|
@@ -92,10 +93,10 @@ export const load = ({ url }: { url: URL }) => deferLocalPage(url, {}, async ()
|
|
|
92
93
|
`,
|
|
93
94
|
"README.md": `# ${label}
|
|
94
95
|
|
|
95
|
-
A module for the
|
|
96
|
-
\`
|
|
97
|
-
repositories, gateway code, startup code, attention badges) and \`pnpm check:modules\`
|
|
98
|
-
boundary
|
|
96
|
+
A module for the Hearthlog platform. It reaches the app only through \`@hearthlog/sdk\`; the
|
|
97
|
+
\`ModuleManifest\` type in \`@hearthlog/contracts\` documents what a module may declare (pages, entities,
|
|
98
|
+
repositories, gateway code, startup code, attention badges), and \`pnpm check:modules\` enforces the
|
|
99
|
+
boundary.
|
|
99
100
|
`,
|
|
100
101
|
};
|
|
101
102
|
}
|
|
@@ -138,7 +139,8 @@ export function exportModule(modulesDirectory: string, name: string, out: string
|
|
|
138
139
|
if (manifest.dependencies && Object.keys(manifest.dependencies).length === 0) delete manifest.dependencies;
|
|
139
140
|
delete manifest.private;
|
|
140
141
|
manifest.license = "AGPL-3.0-only";
|
|
141
|
-
|
|
142
|
+
// The repository the module leaves: its pnpm and test tooling, and its license.
|
|
143
|
+
const root = JSON.parse(readFileSync(join(PLATFORM_ROOT, "package.json"), "utf8")) as { devDependencies?: Record<string, string>; packageManager?: string };
|
|
142
144
|
const publishable = {
|
|
143
145
|
...manifest,
|
|
144
146
|
repository: { type: "git", url: `git+https://github.com/hearthlog/hearthlog-module-${name}.git` },
|
|
@@ -160,7 +162,7 @@ export function exportModule(modulesDirectory: string, name: string, out: string
|
|
|
160
162
|
writeFileSync(join(out, "pnpm-workspace.yaml"), "onlyBuiltDependencies:\n - esbuild\n");
|
|
161
163
|
copyFileSync(join(TEMPLATE, "ci.yml.tmpl"), join(out, ".github", "workflows", "ci.yml"));
|
|
162
164
|
copyFileSync(join(TEMPLATE, "release.yml.tmpl"), join(out, ".github", "workflows", "release.yml"));
|
|
163
|
-
cpSync(join(
|
|
165
|
+
cpSync(join(PLATFORM_ROOT, "LICENSE"), join(out, "LICENSE"));
|
|
164
166
|
const readme = join(out, "README.md");
|
|
165
167
|
const existing = existsSync(readme) ? readFileSync(readme, "utf8").trimEnd() : `# ${title(name)}`;
|
|
166
168
|
writeFileSync(readme, `${existing}\n\n## Status\n\nIt installs against \`@hearthlog/sdk\` ${platformRange}.\n\n\`\`\`bash\npnpm install\npnpm test # generates a site that enables this module, installs it, builds it, then runs the unit tests\n\`\`\`\n\nRelease: set \`version\` in \`package.json\`, commit, and push the tag \`v<version>\`; the release workflow publishes with provenance after approval.\n`);
|
|
@@ -130,7 +130,7 @@ export async function deploySite(options: Options): Promise<void> {
|
|
|
130
130
|
async function deployOwnPublicWorkers(site: ResolvedSite, dryRun: boolean, skipBuild: boolean): Promise<void> {
|
|
131
131
|
if (!site.deployment) return;
|
|
132
132
|
const args = ["deploy", ...(dryRun ? ["--dry-run"] : [])];
|
|
133
|
-
if (site.features.includes("gallery")) {
|
|
133
|
+
if ((site.features as readonly string[]).includes("gallery")) {
|
|
134
134
|
if (skipBuild) {
|
|
135
135
|
// A release built elsewhere carries its Worker bundle; only the configs are written here.
|
|
136
136
|
await generateHostConfigs(site.id);
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
*
|
|
18
18
|
* pnpm build:generated [--site <id>] [--all-modules] [--check] [--force]
|
|
19
19
|
*
|
|
20
|
-
* The site defaults to VITE_STOR_TENANT, then "st". `--all-modules` writes the catalog of every installed
|
|
20
|
+
* The site defaults to VITE_STOR_TENANT, then the host's only site, then "st". `--all-modules` writes the catalog of every installed
|
|
21
21
|
* module and nothing else (`pnpm lock:modules` reads it). `--check` fails when the outputs are stale.
|
|
22
22
|
*/
|
|
23
23
|
|
|
@@ -29,7 +29,7 @@ import { fileURLToPath } from "node:url";
|
|
|
29
29
|
|
|
30
30
|
import { generateCatalog, GENERATED_DIRECTORY, loadModules, modulesInLock, siteDirectory } from "../compose/compose-modules.js";
|
|
31
31
|
import { packageDirectory } from "../host/write-host.js";
|
|
32
|
-
import { hostRoot } from "../root.js";
|
|
32
|
+
import { activeSiteId, hostRoot } from "../root.js";
|
|
33
33
|
|
|
34
34
|
const ROOT = hostRoot();
|
|
35
35
|
const STATE_PATH = join(GENERATED_DIRECTORY, ".build.json");
|
|
@@ -153,7 +153,7 @@ export async function build(options: BuildOptions): Promise<"built" | "current">
|
|
|
153
153
|
|
|
154
154
|
async function main(argv: string[]): Promise<void> {
|
|
155
155
|
const site = argv.indexOf("--site");
|
|
156
|
-
const siteId = (site >= 0 ? argv[site + 1] : undefined) ?? (
|
|
156
|
+
const siteId = (site >= 0 ? argv[site + 1] : undefined) ?? activeSiteId();
|
|
157
157
|
const outcome = await build({ siteId, all: argv.includes("--all-modules"), check: argv.includes("--check"), force: argv.includes("--force") });
|
|
158
158
|
if (outcome === "current" && !argv.includes("--quiet")) console.log(`Generated files are current for ${siteId}.`);
|
|
159
159
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Resolves one site from tenants.json and emits the runtime module the
|
|
6
6
|
* browser app consumes, plus the tenant-branded app shell. The site is
|
|
7
|
-
* selected with VITE_STOR_TENANT (default: "st").
|
|
7
|
+
* selected with VITE_STOR_TENANT (default: the host's only site, else "st").
|
|
8
8
|
*
|
|
9
9
|
* Usage: VITE_STOR_TENANT=bravo pnpm build:generated
|
|
10
10
|
*
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
} from "../tenants/tenant-config.js";
|
|
28
28
|
import { generateHostConfigs } from "../tenants/host-configs.js";
|
|
29
29
|
import { generateTenantWranglerConfigs } from "../tenants/wrangler-config.js";
|
|
30
|
+
import { activeSiteId } from "../root.js";
|
|
30
31
|
import { brandMarkMaskUrl, wordmarkParts } from "@hearthlog/contracts/tenant/brand";
|
|
31
32
|
import {
|
|
32
33
|
themeVariablesCss,
|
|
@@ -137,7 +138,7 @@ if (/%%BRAND_[A-Z_]+%%/.test(brandable)) {
|
|
|
137
138
|
}
|
|
138
139
|
|
|
139
140
|
if (process.env.STOR_SKIP_TENANT_GENERATION !== "1") {
|
|
140
|
-
await generateTenant(
|
|
141
|
+
await generateTenant(activeSiteId(), {
|
|
141
142
|
generateWranglerConfigs: process.env.STOR_WRANGLER_CONFIGS !== "0",
|
|
142
143
|
});
|
|
143
144
|
}
|
package/src/host/write-host.ts
CHANGED
|
@@ -19,7 +19,7 @@ import { createRequire } from "node:module";
|
|
|
19
19
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
20
20
|
import { fileURLToPath } from "node:url";
|
|
21
21
|
|
|
22
|
-
import { hostRoot } from "../root.js";
|
|
22
|
+
import { activeSiteId, hostRoot } from "../root.js";
|
|
23
23
|
import { CORE_ROUTE_FILES } from "./core-routes.js";
|
|
24
24
|
|
|
25
25
|
const ROOT = hostRoot();
|
|
@@ -112,7 +112,7 @@ export async function writeHost(siteId: string): Promise<void> {
|
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
115
|
-
writeHost(
|
|
115
|
+
writeHost(activeSiteId()).catch((error: unknown) => {
|
|
116
116
|
console.error(error instanceof Error ? error.message : String(error));
|
|
117
117
|
process.exitCode = 1;
|
|
118
118
|
});
|
package/src/root.ts
CHANGED
|
@@ -1,5 +1,35 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The platform checkout this tooling runs from: the repository with `packages/`, `scripts/`, and the license. Only the
|
|
7
|
+
* repository's own checks and exports use it; everything a site owns is under `hostRoot()`.
|
|
8
|
+
*/
|
|
9
|
+
export const PLATFORM_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../../..");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* The site a build works on: `VITE_STOR_TENANT` when it is set, otherwise the host's only site (a repository
|
|
13
|
+
* made by `hearthlog init` or `hearthlog export` holds one), otherwise "st", the development site of the platform.
|
|
14
|
+
*/
|
|
15
|
+
export function activeSiteId(root: string = hostRoot()): string {
|
|
16
|
+
const named = process.env.VITE_STOR_TENANT?.trim();
|
|
17
|
+
if (named) return named;
|
|
18
|
+
const sites = new Set<string>();
|
|
19
|
+
const tenants = join(root, "tenants");
|
|
20
|
+
if (existsSync(tenants)) {
|
|
21
|
+
for (const entry of readdirSync(tenants, { withFileTypes: true })) {
|
|
22
|
+
if (entry.isDirectory() && existsSync(join(tenants, entry.name, "tenant.json"))) sites.add(entry.name);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
try {
|
|
26
|
+
const listed = (JSON.parse(readFileSync(join(root, "tenants.json"), "utf8")) as { sites?: Array<{ id?: unknown }> }).sites ?? [];
|
|
27
|
+
for (const site of listed) if (typeof site.id === "string") sites.add(site.id);
|
|
28
|
+
} catch {
|
|
29
|
+
// No tenants.json, or one the tenant configuration check reports: the directories decide.
|
|
30
|
+
}
|
|
31
|
+
return sites.size === 1 ? [...sites][0]! : "st";
|
|
32
|
+
}
|
|
3
33
|
|
|
4
34
|
/**
|
|
5
35
|
* The repository a command works on: the nearest directory, from the working directory upward, that
|
|
@@ -81,7 +81,7 @@ export async function generateHostConfigs(stack: Stack): Promise<string[]> {
|
|
|
81
81
|
infrastructure = infrastructureFor(site, shared);
|
|
82
82
|
ledgerAccount = site.deployment.accountId;
|
|
83
83
|
calendarSites = [site.id];
|
|
84
|
-
gallery = site.features.includes("gallery");
|
|
84
|
+
gallery = (site.features as readonly string[]).includes("gallery");
|
|
85
85
|
} else if (calendarSites.length === 0) {
|
|
86
86
|
return written;
|
|
87
87
|
}
|
|
@@ -105,7 +105,7 @@ export interface InfrastructureDefinition {
|
|
|
105
105
|
ledgerAdminEmail: string;
|
|
106
106
|
/** Shared Cloudflare Access team origin pushed to every site Worker. */
|
|
107
107
|
accessTeamDomain: string;
|
|
108
|
-
/** The Cloudflare account that holds the shared stack
|
|
108
|
+
/** The Cloudflare account that holds the shared stack. */
|
|
109
109
|
accountId?: string;
|
|
110
110
|
/** Browser application and team-wide SSO lifetimes, in whole hours. */
|
|
111
111
|
accessSessionDuration?: string;
|
package/src/vite/config.ts
CHANGED
|
@@ -10,6 +10,7 @@ import { readFileSync } from 'node:fs';
|
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { join, resolve } from 'node:path';
|
|
12
12
|
import { localGatewayOriginAllowed, readLocalLedgerCredentialSync } from '../dev/local-development.js';
|
|
13
|
+
import { activeSiteId } from '../root.js';
|
|
13
14
|
import {
|
|
14
15
|
e2eLedgerStubPlugin,
|
|
15
16
|
localAiDecisionsPlugin,
|
|
@@ -21,7 +22,17 @@ import {
|
|
|
21
22
|
localReceivedInvoiceFilesPlugin,
|
|
22
23
|
} from './dev-plugins.js';
|
|
23
24
|
|
|
24
|
-
import { defineConfig, type ProxyOptions, type UserConfig } from 'vite';
|
|
25
|
+
import { defineConfig, searchForWorkspaceRoot, type ProxyOptions, type UserConfig } from 'vite';
|
|
26
|
+
|
|
27
|
+
/** The `@hearthlog/*` packages the site depends on (the platform and its modules). */
|
|
28
|
+
function hearthlogDependencies(): string[] {
|
|
29
|
+
try {
|
|
30
|
+
const manifest = JSON.parse(readFileSync('package.json', 'utf8')) as { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
|
|
31
|
+
return Object.keys({ ...manifest.dependencies, ...manifest.devDependencies }).filter((name) => name.startsWith('@hearthlog/') && name !== '@hearthlog/generated');
|
|
32
|
+
} catch {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
}
|
|
25
36
|
|
|
26
37
|
function installedVersion(name: string): string {
|
|
27
38
|
try {
|
|
@@ -65,7 +76,10 @@ const buildTimestamp = process.env.STOR_BUILD_TIMESTAMP?.trim() || new Date().to
|
|
|
65
76
|
// Tenant build selection: VITE_STOR_TENANT=bravo writes the site into
|
|
66
77
|
// build/bravo (matching wrangler env.bravo assets.directory). The default
|
|
67
78
|
// tenant keeps the historical ./build output used by the top-level Worker.
|
|
68
|
-
|
|
79
|
+
// Without VITE_STOR_TENANT a host with one site builds that site; the value is
|
|
80
|
+
// set here so the plugins and `import.meta.env` (the service worker) agree.
|
|
81
|
+
const tenantId = activeSiteId();
|
|
82
|
+
process.env.VITE_STOR_TENANT = tenantId;
|
|
69
83
|
const buildDirectory = process.env.STOR_BUILD_OUTPUT_ROOT?.trim() ||
|
|
70
84
|
(tenantId === 'st' ? 'build' : `build/${tenantId}`);
|
|
71
85
|
const svelteKitOutDir = process.env.STOR_SVELTEKIT_OUT_DIR?.trim() || '.svelte-kit';
|
|
@@ -228,8 +242,9 @@ return defineConfig(({ command, mode }): UserConfig => {
|
|
|
228
242
|
},
|
|
229
243
|
server: {
|
|
230
244
|
...devServerProxy,
|
|
231
|
-
// Module and SDK sources live outside src/; Kit's default list omits them.
|
|
232
|
-
|
|
245
|
+
// Module and SDK sources live outside src/; Kit's default list omits them. In a workspace the platform's packages
|
|
246
|
+
// are linked from outside the site, so the workspace root is allowed too.
|
|
247
|
+
fs: { allow: [resolve('modules'), resolve('packages'), searchForWorkspaceRoot(process.cwd())] }
|
|
233
248
|
},
|
|
234
249
|
resolve: {
|
|
235
250
|
alias: {
|
|
@@ -293,7 +308,9 @@ return defineConfig(({ command, mode }): UserConfig => {
|
|
|
293
308
|
},
|
|
294
309
|
],
|
|
295
310
|
optimizeDeps: {
|
|
296
|
-
|
|
311
|
+
// The platform and the modules ship Svelte and TypeScript source; installed from the registry they sit in node_modules,
|
|
312
|
+
// where the dependency scan cannot resolve their relative imports. They are compiled with the site, not pre-bundled.
|
|
313
|
+
exclude: ['sqlocal', 'microlighter', ...hearthlogDependencies()]
|
|
297
314
|
},
|
|
298
315
|
build: {
|
|
299
316
|
// Bundle Microlighter's lazy grammars, which Vite skips in dependencies by default.
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import type { Plugin } from 'vite';
|
|
10
10
|
|
|
11
11
|
import { themeVariablesFor } from '../../../contracts/src/tenant/theme.js';
|
|
12
|
+
import { activeSiteId } from '../root.js';
|
|
12
13
|
import { loadTenantConfig, siteById } from '../tenants/tenant-config.js';
|
|
13
14
|
|
|
14
15
|
const MANIFEST_PATH = '/manifest.webmanifest';
|
|
@@ -40,7 +41,7 @@ async function manifestJson(siteId: string): Promise<string> {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
export function tenantManifestPlugin(): Plugin {
|
|
43
|
-
const siteId =
|
|
44
|
+
const siteId = activeSiteId();
|
|
44
45
|
return {
|
|
45
46
|
name: 'tenant-manifest',
|
|
46
47
|
configureServer(server) {
|