ras-stack 0.16.0 → 0.18.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 +29 -0
- package/dist/build/cli.d.ts +2 -0
- package/dist/build/cli.js +25 -0
- package/dist/build/cli.js.map +1 -0
- package/dist/build/index.d.ts +17 -0
- package/dist/build/index.js +95 -0
- package/dist/build/index.js.map +1 -0
- package/package.json +6 -1
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
|
|
@@ -367,6 +381,21 @@ release:
|
|
|
367
381
|
secrets: inherit
|
|
368
382
|
```
|
|
369
383
|
|
|
384
|
+
Browser jobs can cache the pinned Playwright payload through the shared setup action. Production-container E2E can use the reusable workflow, while repository-specific preparation and the actual test command remain inputs:
|
|
385
|
+
|
|
386
|
+
```yaml
|
|
387
|
+
e2e:
|
|
388
|
+
uses: richardsolomou/ras-stack/.github/workflows/check-container-browser.yml@v0.18.0
|
|
389
|
+
with:
|
|
390
|
+
image: my-app-e2e
|
|
391
|
+
cache-scope: my-app-e2e
|
|
392
|
+
prepare-command: just prepare-e2e
|
|
393
|
+
command: just e2e-run
|
|
394
|
+
just-version: '1.58.0'
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
The loaded image tag is also available to the command as `RAS_STACK_TEST_IMAGE`. Build and browser durations are written to the job summary, and failure artifacts remain configurable. Applications that need extra caches, services, registry publication, or a different PR/main topology can use `actions/build-container` and `actions/setup-playwright` inside their own job instead.
|
|
398
|
+
|
|
370
399
|
The workflow consumes pending changesets, commits the resulting versions and changelogs, pushes the commit and tag atomically, and creates a GitHub Release. It does nothing when no versioned changeset is present. The caller owns its checks, Changesets configuration, release policy, and any deployment that follows the release.
|
|
371
400
|
|
|
372
401
|
Pin actions and reusable workflows to a release tag and let Dependabot propose upgrades.
|
|
@@ -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.
|
|
3
|
+
"version": "0.18.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"
|