four-stroke 0.2.2 → 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.
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 --report-unused-disable-directives-severity=warn");
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,45 +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
- // Judge a wrapped comment by its first line, so the second can carry on the sentence.
68
- "capitalized-comments": ["warn", "always", { ignoreConsecutiveComments: true }],
69
- // Assertion helpers are `assert` or named `expect…` (`expectRedirect`), so it
70
- // can see them; anything left over really asserts nothing.
71
- "vitest/expect-expect": ["warn", { assertFunctionNames: ["expect", "expect*", "assert"] }],
72
- // Route tests are titled by the request they make: "GET /thing", not "gET /thing".
73
- "vitest/prefer-lowercase-title": [
74
- "warn",
75
- { allowedPrefixes: ["GET", "POST", "PUT", "DELETE", "PATCH", "JWT"] },
76
- ],
77
- "react/react-in-jsx-scope": "off",
78
- "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`)
79
68
  "react/function-component-definition": [
80
69
  "error",
81
- {
82
- namedComponents: "arrow-function",
83
- unnamedComponents: "arrow-function",
84
- },
70
+ { namedComponents: "arrow-function", unnamedComponents: "arrow-function" },
85
71
  ],
86
72
  },
87
73
  overrides: [
88
74
  {
89
- 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"],
90
86
  rules: {
91
- "@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",
92
115
  // A helper belongs next to the only test that uses it; hoisting it to module
93
116
  // scope saves nothing in a test run and puts it out of sight.
94
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",
95
125
  },
96
126
  },
97
127
  {
@@ -100,13 +130,10 @@ export default defineConfig({
100
130
  "no-unused-vars": "off",
101
131
  "@typescript-eslint/no-redundant-type-constituents": "off",
102
132
  "@typescript-eslint/no-duplicate-type-constituents": "off",
103
- "group-exports": "off",
104
- "exports-last": "off",
105
- "consistent-indexed-object-style": "off",
106
133
  },
107
134
  },
108
135
  {
109
- files: ["bin/*.mjs", "src/cmd.mjs"],
136
+ files: ["bin/*.mjs", "src/cmd.mjs", "src/openapi-typescript.mjs"],
110
137
  rules: {
111
138
  "import/no-nodejs-modules": "off",
112
139
  "prefer-destructuring": "off",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "four-stroke",
3
- "version": "0.2.2",
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
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.70.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": {
@@ -43,5 +44,12 @@
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
  }
@@ -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;