ras-stack 0.16.0 → 0.17.0

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/README.md CHANGED
@@ -255,6 +255,20 @@ await startTusUpload(upload)
255
255
 
256
256
  Applications retain ownership of email templates, missing-email behavior, upload metadata, authorization, quotas, and completion processing.
257
257
 
258
+ Production server assets can be declared in `ras-stack.assets.json` instead of appended shell copy chains:
259
+
260
+ ```json
261
+ {
262
+ "outputDirectory": ".output/server",
263
+ "assets": [
264
+ { "source": "drizzle", "destination": "drizzle" },
265
+ { "source": "drizzle-postgres", "destination": "drizzle-postgres" }
266
+ ]
267
+ }
268
+ ```
269
+
270
+ Run `ras-stack-assets sync` after the application build and `ras-stack-assets check` before packaging. Sync replaces each declared destination, and check compares the exact file set and content. Sources stay inside the repository, destinations stay inside the output directory, overlapping destinations and symbolic links are rejected, and the application retains ownership of asset generation and contents.
271
+
258
272
  Stateful development servers can reuse one typed resource across HMR without application-specific `globalThis` casts:
259
273
 
260
274
  ```ts
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { checkServerAssets, loadServerAssetsConfig, syncServerAssets } from './index.js';
3
+ const command = process.argv[2];
4
+ const configFile = process.argv[3];
5
+ if (command !== 'check' && command !== 'sync') {
6
+ console.error('usage: ras-stack-assets <check|sync> [config-file]');
7
+ process.exitCode = 2;
8
+ }
9
+ else {
10
+ const root = process.cwd();
11
+ const config = await loadServerAssetsConfig(root, configFile);
12
+ if (command === 'sync') {
13
+ await syncServerAssets(root, config);
14
+ }
15
+ else {
16
+ const drift = await checkServerAssets(root, config);
17
+ for (const destination of drift)
18
+ console.error(`server asset drift: ${destination}`);
19
+ if (drift.length > 0) {
20
+ console.error('run ras-stack-assets sync after the production build');
21
+ process.exitCode = 1;
22
+ }
23
+ }
24
+ }
25
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/build/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAA;AAExF,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAC/B,MAAM,UAAU,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;AAClC,IAAI,OAAO,KAAK,OAAO,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;IAC9C,OAAO,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAA;IACnE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;AACtB,CAAC;KAAM,CAAC;IACN,MAAM,IAAI,GAAG,OAAO,CAAC,GAAG,EAAE,CAAA;IAC1B,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IAC7D,IAAI,OAAO,KAAK,MAAM,EAAE,CAAC;QACvB,MAAM,gBAAgB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;IACtC,CAAC;SAAM,CAAC;QACN,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAA;QACnD,KAAK,MAAM,WAAW,IAAI,KAAK;YAAE,OAAO,CAAC,KAAK,CAAC,uBAAuB,WAAW,EAAE,CAAC,CAAA;QACpF,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,KAAK,CAAC,sDAAsD,CAAC,CAAA;YACrE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAA;QACtB,CAAC;IACH,CAAC;AACH,CAAC","sourcesContent":["#!/usr/bin/env node\nimport { checkServerAssets, loadServerAssetsConfig, syncServerAssets } from './index.js'\n\nconst command = process.argv[2]\nconst configFile = process.argv[3]\nif (command !== 'check' && command !== 'sync') {\n console.error('usage: ras-stack-assets <check|sync> [config-file]')\n process.exitCode = 2\n} else {\n const root = process.cwd()\n const config = await loadServerAssetsConfig(root, configFile)\n if (command === 'sync') {\n await syncServerAssets(root, config)\n } else {\n const drift = await checkServerAssets(root, config)\n for (const destination of drift) console.error(`server asset drift: ${destination}`)\n if (drift.length > 0) {\n console.error('run ras-stack-assets sync after the production build')\n process.exitCode = 1\n }\n }\n}\n"]}
@@ -0,0 +1,17 @@
1
+ export type ServerAsset = {
2
+ source: string;
3
+ destination: string;
4
+ };
5
+ export type ServerAssetsConfig = {
6
+ outputDirectory: string;
7
+ assets: ServerAsset[];
8
+ };
9
+ export declare function loadServerAssetsConfig(root: string, configFile?: string): Promise<{
10
+ outputDirectory: string;
11
+ assets: {
12
+ source: any;
13
+ destination: any;
14
+ }[];
15
+ }>;
16
+ export declare function syncServerAssets(root: string, config: ServerAssetsConfig): Promise<void>;
17
+ export declare function checkServerAssets(root: string, config: ServerAssetsConfig): Promise<string[]>;
@@ -0,0 +1,95 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { cp, lstat, mkdir, readFile, readdir, rm } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ export async function loadServerAssetsConfig(root, configFile = 'ras-stack.assets.json') {
5
+ const source = JSON.parse(await readFile(path.resolve(root, configFile), 'utf8'));
6
+ if (!source || typeof source !== 'object' || !('outputDirectory' in source) || !('assets' in source)) {
7
+ throw new Error(`${configFile} must contain outputDirectory and assets`);
8
+ }
9
+ if (typeof source.outputDirectory !== 'string' || !Array.isArray(source.assets)) {
10
+ throw new Error(`${configFile} must contain a string outputDirectory and an assets array`);
11
+ }
12
+ const assets = source.assets.map((asset, index) => {
13
+ if (!asset ||
14
+ typeof asset !== 'object' ||
15
+ !('source' in asset) ||
16
+ typeof asset.source !== 'string' ||
17
+ !('destination' in asset) ||
18
+ typeof asset.destination !== 'string') {
19
+ throw new Error(`${configFile} assets[${index}] must contain string source and destination paths`);
20
+ }
21
+ return { source: asset.source, destination: asset.destination };
22
+ });
23
+ return { outputDirectory: source.outputDirectory, assets };
24
+ }
25
+ export async function syncServerAssets(root, config) {
26
+ const paths = assetPaths(root, config);
27
+ await Promise.all(paths.map(async (asset) => {
28
+ await manifest(asset.source);
29
+ await rm(asset.destination, { force: true, recursive: true });
30
+ await mkdir(path.dirname(asset.destination), { recursive: true });
31
+ await cp(asset.source, asset.destination, { recursive: true });
32
+ }));
33
+ }
34
+ export async function checkServerAssets(root, config) {
35
+ const compared = await Promise.all(assetPaths(root, config).map(async (asset) => {
36
+ const [source, destination] = await Promise.all([manifest(asset.source), manifest(asset.destination).catch(() => undefined)]);
37
+ return !destination || !equalManifest(source, destination) ? asset.label : undefined;
38
+ }));
39
+ return compared.filter((destination) => destination !== undefined);
40
+ }
41
+ function assetPaths(root, config) {
42
+ const repository = path.resolve(root);
43
+ const output = containedPath(repository, config.outputDirectory, 'outputDirectory');
44
+ const assets = config.assets.map((asset, index) => ({
45
+ source: containedPath(repository, asset.source, `assets[${index}].source`),
46
+ destination: containedPath(output, asset.destination, `assets[${index}].destination`),
47
+ label: asset.destination,
48
+ }));
49
+ for (const [index, asset] of assets.entries()) {
50
+ for (const other of assets.slice(index + 1)) {
51
+ if (contains(asset.destination, other.destination) || contains(other.destination, asset.destination)) {
52
+ throw new Error(`asset destinations must not overlap: ${asset.label} and ${other.label}`);
53
+ }
54
+ }
55
+ }
56
+ return assets;
57
+ }
58
+ function contains(parent, child) {
59
+ const relative = path.relative(parent, child);
60
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
61
+ }
62
+ function containedPath(parent, child, name) {
63
+ if (!child.trim())
64
+ throw new Error(`${name} must not be empty`);
65
+ const resolved = path.resolve(parent, child);
66
+ const relative = path.relative(parent, resolved);
67
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
68
+ throw new Error(`${name} must stay inside ${parent}`);
69
+ }
70
+ return resolved;
71
+ }
72
+ async function manifest(root) {
73
+ const files = new Map();
74
+ const visit = async (current) => {
75
+ const info = await lstat(current);
76
+ if (info.isSymbolicLink())
77
+ throw new Error(`server assets must not contain symbolic links: ${current}`);
78
+ if (info.isFile()) {
79
+ files.set(path.relative(root, current) || '.', createHash('sha256')
80
+ .update(await readFile(current))
81
+ .digest('hex'));
82
+ return;
83
+ }
84
+ if (!info.isDirectory())
85
+ throw new Error(`server assets must contain only files and directories: ${current}`);
86
+ const entries = await readdir(current);
87
+ await Promise.all(entries.map((entry) => visit(path.join(current, entry))));
88
+ };
89
+ await visit(root);
90
+ return files;
91
+ }
92
+ function equalManifest(left, right) {
93
+ return left.size === right.size && [...left].every(([name, digest]) => right.get(name) === digest);
94
+ }
95
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/build/index.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash } from 'node:crypto'\nimport { cp, lstat, mkdir, readFile, readdir, rm } from 'node:fs/promises'\nimport path from 'node:path'\n\nexport type ServerAsset = { source: string; destination: string }\nexport type ServerAssetsConfig = { outputDirectory: string; assets: ServerAsset[] }\n\nexport async function loadServerAssetsConfig(root: string, configFile = 'ras-stack.assets.json') {\n const source = JSON.parse(await readFile(path.resolve(root, configFile), 'utf8')) as unknown\n if (!source || typeof source !== 'object' || !('outputDirectory' in source) || !('assets' in source)) {\n throw new Error(`${configFile} must contain outputDirectory and assets`)\n }\n if (typeof source.outputDirectory !== 'string' || !Array.isArray(source.assets)) {\n throw new Error(`${configFile} must contain a string outputDirectory and an assets array`)\n }\n const assets = source.assets.map((asset, index) => {\n if (\n !asset ||\n typeof asset !== 'object' ||\n !('source' in asset) ||\n typeof asset.source !== 'string' ||\n !('destination' in asset) ||\n typeof asset.destination !== 'string'\n ) {\n throw new Error(`${configFile} assets[${index}] must contain string source and destination paths`)\n }\n return { source: asset.source, destination: asset.destination }\n })\n return { outputDirectory: source.outputDirectory, assets } satisfies ServerAssetsConfig\n}\n\nexport async function syncServerAssets(root: string, config: ServerAssetsConfig) {\n const paths = assetPaths(root, config)\n await Promise.all(\n paths.map(async (asset) => {\n await manifest(asset.source)\n await rm(asset.destination, { force: true, recursive: true })\n await mkdir(path.dirname(asset.destination), { recursive: true })\n await cp(asset.source, asset.destination, { recursive: true })\n }),\n )\n}\n\nexport async function checkServerAssets(root: string, config: ServerAssetsConfig) {\n const compared = await Promise.all(\n assetPaths(root, config).map(async (asset) => {\n const [source, destination] = await Promise.all([manifest(asset.source), manifest(asset.destination).catch(() => undefined)])\n return !destination || !equalManifest(source, destination) ? asset.label : undefined\n }),\n )\n return compared.filter((destination) => destination !== undefined)\n}\n\nfunction assetPaths(root: string, config: ServerAssetsConfig) {\n const repository = path.resolve(root)\n const output = containedPath(repository, config.outputDirectory, 'outputDirectory')\n const assets = config.assets.map((asset, index) => ({\n source: containedPath(repository, asset.source, `assets[${index}].source`),\n destination: containedPath(output, asset.destination, `assets[${index}].destination`),\n label: asset.destination,\n }))\n for (const [index, asset] of assets.entries()) {\n for (const other of assets.slice(index + 1)) {\n if (contains(asset.destination, other.destination) || contains(other.destination, asset.destination)) {\n throw new Error(`asset destinations must not overlap: ${asset.label} and ${other.label}`)\n }\n }\n }\n return assets\n}\n\nfunction contains(parent: string, child: string) {\n const relative = path.relative(parent, child)\n return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))\n}\n\nfunction containedPath(parent: string, child: string, name: string) {\n if (!child.trim()) throw new Error(`${name} must not be empty`)\n const resolved = path.resolve(parent, child)\n const relative = path.relative(parent, resolved)\n if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {\n throw new Error(`${name} must stay inside ${parent}`)\n }\n return resolved\n}\n\nasync function manifest(root: string) {\n const files = new Map<string, string>()\n const visit = async (current: string) => {\n const info = await lstat(current)\n if (info.isSymbolicLink()) throw new Error(`server assets must not contain symbolic links: ${current}`)\n if (info.isFile()) {\n files.set(\n path.relative(root, current) || '.',\n createHash('sha256')\n .update(await readFile(current))\n .digest('hex'),\n )\n return\n }\n if (!info.isDirectory()) throw new Error(`server assets must contain only files and directories: ${current}`)\n const entries = await readdir(current)\n await Promise.all(entries.map((entry) => visit(path.join(current, entry))))\n }\n await visit(root)\n return files\n}\n\nfunction equalManifest(left: Map<string, string>, right: Map<string, string>) {\n return left.size === right.size && [...left].every(([name, digest]) => right.get(name) === digest)\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ras-stack",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Composable full-stack primitives shared across Richard Solomou's applications.",
5
5
  "keywords": [
6
6
  "authentication",
@@ -17,6 +17,7 @@
17
17
  "url": "git+https://github.com/richardsolomou/ras-stack.git"
18
18
  },
19
19
  "bin": {
20
+ "ras-stack-assets": "./dist/build/cli.js",
20
21
  "ras-stack-policy": "./dist/policy/cli.js"
21
22
  },
22
23
  "files": [
@@ -42,6 +43,10 @@
42
43
  "types": "./dist/email/index.d.ts",
43
44
  "default": "./dist/email/index.js"
44
45
  },
46
+ "./build": {
47
+ "types": "./dist/build/index.d.ts",
48
+ "default": "./dist/build/index.js"
49
+ },
45
50
  "./database": {
46
51
  "types": "./dist/database/index.d.ts",
47
52
  "default": "./dist/database/index.js"