four-stroke 0.2.1 → 0.3.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.
@@ -12,3 +12,5 @@ updates:
12
12
  schedule:
13
13
  interval: "daily"
14
14
  time: "05:08"
15
+ ignore:
16
+ - dependency-name: "wrangler"
package/bin/api-types.mjs CHANGED
@@ -1,15 +1,23 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import ts from "@typescript/typescript6";
3
4
  import fs from "node:fs";
4
- import openapiTS from "openapi-typescript";
5
5
  import { format } from "oxfmt";
6
- import ts from "typescript";
6
+ import openapiTS from "../src/openapi-typescript.mjs";
7
7
 
8
8
  const services = process.argv[2].split(",");
9
9
 
10
10
  const UUID = ts.factory.createTypeReferenceNode(ts.factory.createIdentifier("UUID"));
11
11
  const NULL = ts.factory.createLiteralTypeNode(ts.factory.createNull());
12
12
 
13
+ const isEmpty = (node) =>
14
+ ts.isTypeAliasDeclaration(node) &&
15
+ ts.isTypeReferenceNode(node.type) &&
16
+ ts.isIdentifier(node.type.typeName) &&
17
+ node.type.typeName.text === "Record" &&
18
+ node.type.typeArguments?.length === 2 &&
19
+ node.type.typeArguments[1].kind === ts.SyntaxKind.NeverKeyword;
20
+
13
21
  await Promise.all(
14
22
  services.map(async (service) => {
15
23
  const output = await openapiTS(`${service}/doc`, {
@@ -20,18 +28,18 @@ await Promise.all(
20
28
  return null;
21
29
  },
22
30
  });
31
+ const path = `src/__definitions__/${service.replace("https://", "").replace("http://", "")}-definitions.ts`;
23
32
  const printer = ts.createPrinter({});
24
- const resultFile = ts.createSourceFile(
25
- `src/__definitions__/${service.replace("https://", "")}-definitions.ts`,
26
- "",
27
- ts.ScriptTarget.Latest,
28
- );
29
- const result = printer.printNode(ts.EmitHint.Unspecified, output[0], resultFile);
33
+ const resultFile = ts.createSourceFile(path, "", ts.ScriptTarget.Latest);
34
+ const result = output
35
+ .filter((node) => !isEmpty(node))
36
+ .map((node) => printer.printNode(ts.EmitHint.Unspecified, node, resultFile))
37
+ .join("\n\n");
30
38
  fs.writeFileSync(
31
- `src/__definitions__/${service.replace("https://", "")}-definitions.ts`,
39
+ path,
32
40
  (
33
41
  await format(
34
- `src/__definitions__/${service.replace("https://", "")}-definitions.ts`,
42
+ path,
35
43
  `// Autogenerated by two-stroke
36
44
 
37
45
  ${fs.readFileSync("src/__definitions__/type-definitions.ts", "utf8")}
package/bin/lint.mjs CHANGED
@@ -3,9 +3,13 @@
3
3
  import fs from "fs";
4
4
  import { cmd } from "../src/cmd.mjs";
5
5
 
6
+ const isApp = fs.readdirSync(".").some((file) => file.startsWith("vite.config."));
7
+
6
8
  if (fs.existsSync("wrangler.jsonc")) {
7
9
  cmd("wrangler types --env-file /dev/null --strict-vars false --check");
8
10
  }
9
- cmd("react-router typegen");
11
+ if (isApp) {
12
+ cmd("react-router typegen");
13
+ }
10
14
  cmd("oxfmt --check");
11
- cmd("oxlint");
15
+ cmd("oxlint --report-unused-disable-directives-severity=error");
package/bin/test.mjs CHANGED
@@ -3,6 +3,7 @@
3
3
  import { cmd } from "../src/cmd.mjs";
4
4
 
5
5
  cmd("vitest", [
6
+ "--passWithNoTests",
6
7
  ...(!process.argv.slice(2).includes("-w") && !process.argv.slice(2).includes("--watch")
7
8
  ? ["--run"]
8
9
  : []),
@@ -3,9 +3,13 @@
3
3
  import fs from "fs";
4
4
  import { cmd } from "../src/cmd.mjs";
5
5
 
6
+ const isApp = fs.readdirSync(".").some((file) => file.startsWith("vite.config."));
7
+
6
8
  if (fs.existsSync("wrangler.jsonc")) {
7
9
  cmd("wrangler types --env-file /dev/null --strict-vars false");
8
10
  }
9
- cmd("react-router typegen");
10
- cmd("vite build");
11
+ if (isApp) {
12
+ cmd("react-router typegen");
13
+ cmd("vite build");
14
+ }
11
15
  cmd("tsc --noEmit");
package/oxlint.config.mjs CHANGED
@@ -1,17 +1,19 @@
1
1
  import { defineConfig } from "oxlint";
2
2
 
3
3
  export default defineConfig({
4
- plugins: ["eslint", "typescript", "unicorn", "oxc", "import", "promise", "vitest", "react"],
4
+ plugins: ["eslint", "typescript", "unicorn", "oxc", "import", "promise", "react"],
5
5
  options: {
6
6
  typeAware: true,
7
7
  },
8
8
  categories: {
9
9
  correctness: "error",
10
- suspicious: "warn",
10
+ suspicious: "error",
11
11
  perf: "error",
12
- style: "warn",
12
+ style: "error",
13
13
  },
14
14
  rules: {
15
+ // Judge a wrapped comment by its first line, so the second can carry on the sentence.
16
+ "capitalized-comments": ["error", "always", { ignoreConsecutiveComments: true }],
15
17
  "no-map-spread": "off",
16
18
  "no-named-export": "off",
17
19
  "no-magic-numbers": "off",
@@ -22,7 +24,6 @@ export default defineConfig({
22
24
  "sort-keys": "off",
23
25
  curly: "off",
24
26
  "switch-case-braces": "off",
25
- "vitest/require-hook": "off",
26
27
  "no-ternary": "off",
27
28
  "group-exports": "off",
28
29
  "exports-last": "off",
@@ -30,7 +31,6 @@ export default defineConfig({
30
31
  "no-null": "off",
31
32
  "consistent-type-specifier-style": "off",
32
33
  "max-statements": "off",
33
- "consistent-type-imports": "error",
34
34
  "relative-url-style": ["error", "always"],
35
35
  "no-namespace": "off",
36
36
  "no-duplicate-imports": ["error", { allowSeparateTypeImports: true }],
@@ -53,32 +53,75 @@ export default defineConfig({
53
53
  "no-anonymous-default-export": "off",
54
54
  "no-continue": "off",
55
55
  "init-declarations": "off",
56
- "vitest/prefer-to-be-truthy": "off",
57
- "vitest/prefer-to-be-falsy": "off",
58
- "vitest/prefer-expect-assertions": "off",
59
- "vitest/prefer-importing-vitest-globals": "off",
60
- "vitest/no-importing-vitest-globals": "error",
61
- "vitest/require-top-level-describe": "off",
62
- "vitest/max-expects": "off",
63
- "vitest/no-hooks": "off",
64
56
  "unicorn/number-literal-case": "off",
65
57
  "eslint/one-var": "off",
66
- "vitest/prefer-called-once": "off",
67
- "react/react-in-jsx-scope": "off",
68
- "react/jsx-max-depth": "off",
58
+ // A ternary only reads better when both branches fit on one line.
59
+ "unicorn/prefer-ternary": ["error", "only-single-line"],
60
+ "unicorn/prefer-spread": "off", // Upstream bug
61
+ // Positional capture groups are used throughout; naming them all would
62
+ // reindex every `match[n]` consumer for no functional gain.
63
+ "eslint/prefer-named-capture-group": "off",
64
+ // Stylesheets can only be pulled in for their side effects.
65
+ "import/no-unassigned-import": ["error", { allow: ["**/*.css", "**/*.scss"] }],
66
+ "react/jsx-max-depth": ["error", { max: 4 }],
67
+ "react/react-in-jsx-scope": "off", // Automatic JSX runtime (tsconfig `jsx: react-jsx`)
69
68
  "react/function-component-definition": [
70
69
  "error",
71
- {
72
- namedComponents: "arrow-function",
73
- unnamedComponents: "arrow-function",
74
- },
70
+ { namedComponents: "arrow-function", unnamedComponents: "arrow-function" },
75
71
  ],
76
72
  },
77
73
  overrides: [
78
74
  {
79
- files: ["*.test.ts", "*.spec.ts"],
75
+ // Mocks and page factories are test code too, and use vitest.
76
+ files: [
77
+ "*.test.ts",
78
+ "*.test.tsx",
79
+ "*.spec.ts",
80
+ "*.spec.tsx",
81
+ "e2e/**",
82
+ "**/__mocks__/**",
83
+ "**/__factories__/**",
84
+ ],
85
+ plugins: ["vitest"],
80
86
  rules: {
81
- "@typescript-eslint/no-explicit-any": "off",
87
+ "vitest/require-hook": "off",
88
+ "vitest/prefer-to-be-truthy": "off",
89
+ "vitest/prefer-to-be-falsy": "off",
90
+ "vitest/prefer-expect-assertions": "off",
91
+ "vitest/prefer-importing-vitest-globals": "off",
92
+ "vitest/prefer-describe-function-title": "off",
93
+ "vitest/require-top-level-describe": "off",
94
+ "vitest/max-expects": "off",
95
+ "vitest/no-hooks": "off",
96
+ "vitest/prefer-called-once": "off",
97
+ // `toEqual` ignores keys written as `undefined`; `toStrictEqual` does not. Fixtures
98
+ // spell those out on purpose, so the fix changes what a test asserts.
99
+ "vitest/prefer-strict-equal": "off",
100
+ // `vi.mock(import("x"))` does not type-check against vi.mock's overloads.
101
+ "vitest/prefer-import-in-mock": "off",
102
+ // A mock standing in for a whole module would restate that module's signatures.
103
+ "vitest/require-mock-type-parameters": "off",
104
+ // Assertion helpers are named `expect…` or `assert…` (`expectRedirect`), or are
105
+ // page-object methods that wrap expect(), so it can see them; anything left over
106
+ // really asserts nothing.
107
+ "vitest/expect-expect": [
108
+ "error",
109
+ { assertFunctionNames: ["expect*", "assert*", "page.expect*", "page.*Present"] },
110
+ ],
111
+ "vitest/prefer-lowercase-title": "off",
112
+ // Tests stash a method to restore it after stubbing. It is never called, so
113
+ // `this` never matters.
114
+ "unbound-method": "off",
115
+ // A helper belongs next to the only test that uses it; hoisting it to module
116
+ // scope saves nothing in a test run and puts it out of sight.
117
+ "unicorn/consistent-function-scoping": "off",
118
+ "vitest/no-conditional-expect": "off",
119
+ "vitest/prefer-called-with": "off",
120
+ "prefer-arrow-callback": "off",
121
+ "vitest/consistent-vitest-vi": "off", // Upstream bug
122
+ "vitest/consistent-test-filename": "off",
123
+ // UI steps, polling and findBy queries are sequential by design.
124
+ "eslint/no-await-in-loop": "off",
82
125
  },
83
126
  },
84
127
  {
@@ -87,13 +130,10 @@ export default defineConfig({
87
130
  "no-unused-vars": "off",
88
131
  "@typescript-eslint/no-redundant-type-constituents": "off",
89
132
  "@typescript-eslint/no-duplicate-type-constituents": "off",
90
- "group-exports": "off",
91
- "exports-last": "off",
92
- "consistent-indexed-object-style": "off",
93
133
  },
94
134
  },
95
135
  {
96
- files: ["bin/*.mjs", "src/cmd.mjs"],
136
+ files: ["bin/*.mjs", "src/cmd.mjs", "src/openapi-typescript.mjs"],
97
137
  rules: {
98
138
  "import/no-nodejs-modules": "off",
99
139
  "prefer-destructuring": "off",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-stroke",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Simple Cloudflare Worker SSR SPA framework",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -20,15 +20,16 @@
20
20
  "main": "src/fake.ts",
21
21
  "dependencies": {
22
22
  "@types/react": "^19.3.0",
23
+ "@typescript/typescript6": "^6.0.2",
23
24
  "openapi-typescript": "^7.13.0",
24
- "typescript": "^6.0.3"
25
+ "typescript": "^7.0.2"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@cloudflare/vitest-pool-workers": "^0.22.0",
28
- "knip": "^6.35.1",
29
- "oxfmt": "^0.67.0",
30
- "oxlint": "^1.82.0",
31
- "oxlint-tsgolint": "^7.0.2001",
29
+ "knip": "^6.38.0",
30
+ "oxfmt": "^0.71.0",
31
+ "oxlint": "^1.86.0",
32
+ "oxlint-tsgolint": "^7.0.2003",
32
33
  "vitest": "^4.1.11"
33
34
  },
34
35
  "peerDependencies": {
@@ -37,11 +38,18 @@
37
38
  "oxfmt": ">=0.59.0",
38
39
  "oxlint": ">=1.74.0",
39
40
  "oxlint-tsgolint": ">=0.24.0",
40
- "typescript": "^6.0.3",
41
+ "typescript": "^7.0.2",
41
42
  "vitest": "^4",
42
43
  "wrangler": ">=3"
43
44
  },
44
45
  "engines": {
45
46
  "node": "^24.9.0"
47
+ },
48
+ "scripts": {
49
+ "dev": "node bin/dev.mjs",
50
+ "format": "node bin/format.mjs",
51
+ "lint": "node bin/lint.mjs",
52
+ "test": "node bin/test.mjs",
53
+ "type-check": "node bin/type-check.mjs"
46
54
  }
47
55
  }
package/src/cmd.mjs CHANGED
@@ -8,7 +8,6 @@ export function cmd(program, args = []) {
8
8
  stdio: "inherit",
9
9
  },
10
10
  );
11
- // eslint-disable-next-line n/no-process-exit
12
11
  if (status) process.exit(status);
13
12
  if (error) throw error;
14
13
  }
@@ -0,0 +1,15 @@
1
+ import { registerHooks } from "node:module";
2
+
3
+ // Openapi-typescript needs the TypeScript 6 JS API (ts.factory), which TypeScript 7 no longer
4
+ // ships. Redirect its `typescript` import to @typescript/typescript6, resolved from two-stroke's
5
+ // own dependencies so this works however two-stroke is installed.
6
+ registerHooks({
7
+ resolve(specifier, context, nextResolve) {
8
+ if (specifier === "typescript" && context.parentURL?.includes("/openapi-typescript/")) {
9
+ return nextResolve("@typescript/typescript6", { ...context, parentURL: import.meta.url });
10
+ }
11
+ return nextResolve(specifier, context);
12
+ },
13
+ });
14
+
15
+ export default (await import("openapi-typescript")).default;