four-stroke 0.0.2
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/.github/actions/install/action.yml +12 -0
- package/.github/dependabot.yml +14 -0
- package/.github/workflows/ci.yml +20 -0
- package/.github/workflows/dependabot.yml +32 -0
- package/.github/workflows/release.yml +24 -0
- package/README.md +9 -0
- package/bin/api-types.mjs +41 -0
- package/bin/deploy.mjs +27 -0
- package/bin/dev.mjs +5 -0
- package/bin/format.mjs +6 -0
- package/bin/lint.mjs +11 -0
- package/bin/test.mjs +10 -0
- package/bin/type-check.mjs +25 -0
- package/eslint.config.js +3 -0
- package/package.json +59 -0
- package/pnpm-workspace.yaml +8 -0
- package/src/cmd.mjs +14 -0
- package/src/components/viewer-menu.tsx +39 -0
- package/src/fake.ts +1 -0
- package/src/util/auth.ts +55 -0
- package/tsconfig.json +23 -0
- package/worker-configuration.d.ts +15795 -0
- package/wrangler.jsonc +5 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Autogenerated by https://github.com/change-engine/ecu/github
|
|
2
|
+
version: 2
|
|
3
|
+
updates:
|
|
4
|
+
- package-ecosystem: "github-actions"
|
|
5
|
+
directory: "/"
|
|
6
|
+
schedule:
|
|
7
|
+
interval: "daily"
|
|
8
|
+
time: "05:08"
|
|
9
|
+
|
|
10
|
+
- package-ecosystem: "npm"
|
|
11
|
+
directory: "/"
|
|
12
|
+
schedule:
|
|
13
|
+
interval: "daily"
|
|
14
|
+
time: "05:08"
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on: [push]
|
|
4
|
+
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
|
|
8
|
+
defaults:
|
|
9
|
+
run:
|
|
10
|
+
shell: bash
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
ci:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v6
|
|
17
|
+
|
|
18
|
+
- uses: ./.github/actions/install
|
|
19
|
+
|
|
20
|
+
- run: pnpm eslint
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Autogenerated by https://github.com/change-engine/ecu/github
|
|
2
|
+
|
|
3
|
+
name: Dependabot auto-approve
|
|
4
|
+
on:
|
|
5
|
+
pull_request_target:
|
|
6
|
+
types: [opened]
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
pull-requests: write
|
|
10
|
+
contents: write
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
dependabot:
|
|
14
|
+
runs-on: ubuntu-slim
|
|
15
|
+
if: ${{ github.actor == 'dependabot[bot]' }}
|
|
16
|
+
steps:
|
|
17
|
+
- name: Dependabot metadata
|
|
18
|
+
id: metadata
|
|
19
|
+
uses: dependabot/fetch-metadata@v3.1.0
|
|
20
|
+
with:
|
|
21
|
+
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
22
|
+
- name: Approve a PR
|
|
23
|
+
if: ${{steps.metadata.outputs.update-type != 'version-update:semver-major'}}
|
|
24
|
+
run: gh pr review --approve "$PR_URL"
|
|
25
|
+
env:
|
|
26
|
+
PR_URL: ${{github.event.pull_request.html_url}}
|
|
27
|
+
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
28
|
+
- name: Enable auto-merge for Dependabot PRs
|
|
29
|
+
run: gh pr merge --auto --merge "$PR_URL"
|
|
30
|
+
env:
|
|
31
|
+
PR_URL: ${{github.event.pull_request.html_url}}
|
|
32
|
+
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
defaults:
|
|
13
|
+
run:
|
|
14
|
+
shell: bash
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
ci:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
steps:
|
|
20
|
+
- uses: actions/checkout@v6
|
|
21
|
+
|
|
22
|
+
- uses: ./.github/actions/install
|
|
23
|
+
|
|
24
|
+
- run: pnpm publish --no-git-checks
|
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import fs from "node:fs";
|
|
4
|
+
import openapiTS from "openapi-typescript";
|
|
5
|
+
import prettier from "prettier";
|
|
6
|
+
import ts from "typescript";
|
|
7
|
+
|
|
8
|
+
const services = process.argv[2].split(",");
|
|
9
|
+
|
|
10
|
+
const UUID = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("UUID"));
|
|
11
|
+
const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());
|
|
12
|
+
|
|
13
|
+
await Promise.all(
|
|
14
|
+
services.map(async (service) => {
|
|
15
|
+
const output = await openapiTS(`${service}/doc`, {
|
|
16
|
+
transform(schemaObject) {
|
|
17
|
+
if ("format" in schemaObject && schemaObject.format === "uuid") {
|
|
18
|
+
return schemaObject.nullable ? ts.factory.createUnionTypeNode([UUID, NULL]) : UUID;
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
});
|
|
22
|
+
const printer = ts.createPrinter({});
|
|
23
|
+
const resultFile = ts.createSourceFile(
|
|
24
|
+
`src/__definitions__/${service.replace("https://", "")}-definitions.ts`,
|
|
25
|
+
"",
|
|
26
|
+
ts.ScriptTarget.Latest,
|
|
27
|
+
);
|
|
28
|
+
const result = printer.printNode(ts.EmitHint.Unspecified, output[0], resultFile);
|
|
29
|
+
fs.writeFileSync(
|
|
30
|
+
`src/__definitions__/${service.replace("https://", "")}-definitions.ts`,
|
|
31
|
+
await prettier.format(
|
|
32
|
+
`// Autogenerated by four-stroke
|
|
33
|
+
|
|
34
|
+
${fs.readFileSync("src/__definitions__/type-definitions.ts", "utf-8")}
|
|
35
|
+
|
|
36
|
+
${result}`,
|
|
37
|
+
{ parser: "typescript", printWidth: 100 },
|
|
38
|
+
),
|
|
39
|
+
);
|
|
40
|
+
}),
|
|
41
|
+
);
|
package/bin/deploy.mjs
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { cmd } from "../src/cmd.mjs";
|
|
5
|
+
import { basename } from "path";
|
|
6
|
+
|
|
7
|
+
const env = process.argv[2];
|
|
8
|
+
const release = process.argv[3];
|
|
9
|
+
|
|
10
|
+
fs.writeFileSync("src/release.ts", `export default "${release}";`);
|
|
11
|
+
|
|
12
|
+
cmd(`wrangler secret bulk /dev/stdin --env ${env}`);
|
|
13
|
+
|
|
14
|
+
cmd(
|
|
15
|
+
`sentry-cli releases --org change-engine --project ${basename(process.cwd())} new ${release} --finalize`,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
cmd(
|
|
19
|
+
`sentry-cli releases --org change-engine --project ${basename(process.cwd())} set-commits ${release} --ignore-missing`,
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
cmd(`vite build`);
|
|
23
|
+
cmd(`wrangler deploy --env ${env} --outdir dist`);
|
|
24
|
+
|
|
25
|
+
cmd(
|
|
26
|
+
`sentry-cli sourcemaps --org change-engine --project ${basename(process.cwd())} upload --release="${release}" dist`,
|
|
27
|
+
);
|
package/bin/dev.mjs
ADDED
package/bin/format.mjs
ADDED
package/bin/lint.mjs
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { cmd } from "../src/cmd.mjs";
|
|
5
|
+
|
|
6
|
+
cmd("react-router typegen");
|
|
7
|
+
if (fs.existsSync("wrangler.jsonc")) {
|
|
8
|
+
cmd("wrangler types --strict-vars false --check");
|
|
9
|
+
}
|
|
10
|
+
cmd("eslint --cache --max-warnings=0");
|
|
11
|
+
cmd("prettier --cache --check .");
|
package/bin/test.mjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { cmd } from "../src/cmd.mjs";
|
|
5
|
+
|
|
6
|
+
if (fs.existsSync("wrangler.jsonc")) {
|
|
7
|
+
const devVarsFiles = fs.globSync("**/.dev.vars");
|
|
8
|
+
const backed = [];
|
|
9
|
+
|
|
10
|
+
for (const file of devVarsFiles) {
|
|
11
|
+
const tmp = file + ".bak.bak";
|
|
12
|
+
fs.renameSync(file, tmp);
|
|
13
|
+
backed.push({ original: file, tmp });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
cmd("wrangler types --strict-vars false");
|
|
18
|
+
} finally {
|
|
19
|
+
for (const { original, tmp } of backed) {
|
|
20
|
+
fs.renameSync(tmp, original);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
cmd("vite build");
|
|
25
|
+
cmd("tsc --noEmit");
|
package/eslint.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"bin": {
|
|
3
|
+
"api-types": "./bin/api-types.mjs",
|
|
4
|
+
"deploy": "./bin/deploy.mjs",
|
|
5
|
+
"dev": "./bin/dev.mjs",
|
|
6
|
+
"format": "./bin/format.mjs",
|
|
7
|
+
"lint": "./bin/lint.mjs",
|
|
8
|
+
"test": "./bin/test.mjs",
|
|
9
|
+
"type-check": "./bin/type-check.mjs"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"@cloudflare/vitest-pool-workers": "^0.16.13",
|
|
13
|
+
"@sentry/cli": "^3.5.0",
|
|
14
|
+
"@types/node": "^25.9.2",
|
|
15
|
+
"@types/react": "^19.2.17",
|
|
16
|
+
"@types/react-dom": "^19.2.3",
|
|
17
|
+
"@typescript-eslint/eslint-plugin": "^8.60.1",
|
|
18
|
+
"@typescript-eslint/parser": "^8.60.1",
|
|
19
|
+
"@vitest/runner": "^4.1.8",
|
|
20
|
+
"@vitest/snapshot": "^4.1.8",
|
|
21
|
+
"eslint-config-prettier": "^10.1.8",
|
|
22
|
+
"eslint-config-two-stroke": "^1.5.4",
|
|
23
|
+
"eslint-config-typescript": "^3.0.0",
|
|
24
|
+
"openapi-typescript": "^7.13.0",
|
|
25
|
+
"react": "^19.2.7",
|
|
26
|
+
"react-dom": "^19.2.7"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@sentry/cli": ">=2",
|
|
30
|
+
"eslint": ">=10",
|
|
31
|
+
"prettier": ">=3",
|
|
32
|
+
"typescript": ">=5",
|
|
33
|
+
"vitest": ">=4",
|
|
34
|
+
"wrangler": ">=3"
|
|
35
|
+
},
|
|
36
|
+
"description": "Simple Cloudflare Worker SSR SPA framework",
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": "^24.9.0"
|
|
39
|
+
},
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"main": "src/index.ts",
|
|
42
|
+
"name": "four-stroke",
|
|
43
|
+
"prettier": {
|
|
44
|
+
"printWidth": 100
|
|
45
|
+
},
|
|
46
|
+
"repository": {
|
|
47
|
+
"type": "git",
|
|
48
|
+
"url": "https://github.com/change-engine/four-stroke"
|
|
49
|
+
},
|
|
50
|
+
"type": "module",
|
|
51
|
+
"version": "0.0.2",
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/eslint": "^9.6.1",
|
|
54
|
+
"eslint": "^10.4.1",
|
|
55
|
+
"prettier": "^3.8.3",
|
|
56
|
+
"typescript": "^6.0.3",
|
|
57
|
+
"vitest": "^4.1.8"
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/cmd.mjs
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { spawnSync } from "child_process";
|
|
2
|
+
|
|
3
|
+
export function cmd(program, args = []) {
|
|
4
|
+
const { error, status } = spawnSync(
|
|
5
|
+
program.split(" ")[0],
|
|
6
|
+
[...program.split(" ").slice(1), ...args],
|
|
7
|
+
{
|
|
8
|
+
stdio: "inherit",
|
|
9
|
+
},
|
|
10
|
+
);
|
|
11
|
+
// eslint-disable-next-line n/no-process-exit
|
|
12
|
+
if (status) process.exit(status);
|
|
13
|
+
if (error) throw error;
|
|
14
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { Viewer } from "../util/auth";
|
|
2
|
+
|
|
3
|
+
type ViewerMenuProps = {
|
|
4
|
+
logoutUrl: string;
|
|
5
|
+
viewer: Viewer;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export default function ViewerMenu({ logoutUrl, viewer }: ViewerMenuProps): React.ReactNode {
|
|
9
|
+
return (
|
|
10
|
+
<details className="group relative">
|
|
11
|
+
<summary className="flex cursor-pointer list-none items-center gap-3 rounded-md px-2 py-1.5 hover:bg-stone-100 focus:outline-none focus:ring-2 focus:ring-emerald-900 focus:ring-offset-2 [&::-webkit-details-marker]:hidden">
|
|
12
|
+
{viewer.picture ? (
|
|
13
|
+
<img
|
|
14
|
+
alt=""
|
|
15
|
+
className="size-9 rounded-full object-cover"
|
|
16
|
+
referrerPolicy="no-referrer"
|
|
17
|
+
src={viewer.picture}
|
|
18
|
+
/>
|
|
19
|
+
) : (
|
|
20
|
+
<span
|
|
21
|
+
aria-hidden="true"
|
|
22
|
+
className="grid size-9 place-items-center rounded-full bg-emerald-950 text-sm font-semibold text-white"
|
|
23
|
+
>
|
|
24
|
+
{viewer.name.slice(0, 1).toUpperCase()}
|
|
25
|
+
</span>
|
|
26
|
+
)}
|
|
27
|
+
<span className="max-w-48 truncate text-sm font-medium">{viewer.name}</span>
|
|
28
|
+
</summary>
|
|
29
|
+
<nav className="absolute right-0 mt-2 w-40 rounded-md border border-stone-200 bg-white py-1 shadow-lg">
|
|
30
|
+
<a
|
|
31
|
+
className="block px-4 py-2 text-sm font-medium text-neutral-700 hover:bg-stone-100 focus:bg-stone-100 focus:outline-none"
|
|
32
|
+
href={logoutUrl}
|
|
33
|
+
>
|
|
34
|
+
Logout
|
|
35
|
+
</a>
|
|
36
|
+
</nav>
|
|
37
|
+
</details>
|
|
38
|
+
);
|
|
39
|
+
}
|
package/src/fake.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export default { fetch: () => {} };
|
package/src/util/auth.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export type Viewer = {
|
|
2
|
+
name: string;
|
|
3
|
+
picture: string | null;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export const getCookieValue = (request: Request, name: string): string | null => {
|
|
7
|
+
const cookieHeader = request.headers.get("Cookie");
|
|
8
|
+
|
|
9
|
+
if (!cookieHeader) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const prefix = `${name}=`;
|
|
14
|
+
const cookie = cookieHeader.split(";").find((part) => part.trim().startsWith(prefix));
|
|
15
|
+
|
|
16
|
+
return cookie ? decodeURIComponent(cookie.trim().slice(prefix.length)) : null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const decodeBase64Url = (value: string): string => {
|
|
20
|
+
const base64 = value.replaceAll("-", "+").replaceAll("_", "/");
|
|
21
|
+
const padded = base64.padEnd(Math.ceil(base64.length / 4) * 4, "=");
|
|
22
|
+
const bytes = Uint8Array.from(atob(padded), (character) => character.charCodeAt(0));
|
|
23
|
+
|
|
24
|
+
return new TextDecoder().decode(bytes);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const parseViewerFromAccessToken = (accessToken: string): Viewer | null => {
|
|
28
|
+
const [, payload] = accessToken.split(".");
|
|
29
|
+
|
|
30
|
+
if (!payload) {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const claims = JSON.parse(decodeBase64Url(payload)) as {
|
|
36
|
+
email?: unknown;
|
|
37
|
+
name?: unknown;
|
|
38
|
+
picture?: unknown;
|
|
39
|
+
};
|
|
40
|
+
const name =
|
|
41
|
+
typeof claims.name === "string" && claims.name.trim().length > 0
|
|
42
|
+
? claims.name
|
|
43
|
+
: typeof claims.email === "string" && claims.email.trim().length > 0
|
|
44
|
+
? claims.email
|
|
45
|
+
: "Signed in";
|
|
46
|
+
const picture =
|
|
47
|
+
typeof claims.picture === "string" && claims.picture.trim().length > 0
|
|
48
|
+
? claims.picture
|
|
49
|
+
: null;
|
|
50
|
+
|
|
51
|
+
return { name, picture };
|
|
52
|
+
} catch {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"erasableSyntaxOnly": true,
|
|
4
|
+
"esModuleInterop": true,
|
|
5
|
+
"isolatedModules": true,
|
|
6
|
+
"lib": ["ESNext"],
|
|
7
|
+
"module": "ESNext",
|
|
8
|
+
"moduleResolution": "bundler",
|
|
9
|
+
"noUncheckedIndexedAccess": true,
|
|
10
|
+
"resolveJsonModule": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"target": "ESNext",
|
|
14
|
+
"types": [
|
|
15
|
+
"./worker-configuration.d.ts",
|
|
16
|
+
"vitest/globals",
|
|
17
|
+
"@cloudflare/vitest-pool-workers/types"
|
|
18
|
+
],
|
|
19
|
+
"verbatimModuleSyntax": true,
|
|
20
|
+
"jsx": "react-jsx"
|
|
21
|
+
},
|
|
22
|
+
"exclude": ["../../dist"]
|
|
23
|
+
}
|