pasika 0.1.4 → 0.2.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 +40 -55
- package/dist/eslint/pasika/index.d.ts +18 -0
- package/dist/eslint/pasika/index.js +21 -3
- package/dist/eslint/pasika/rules/enforce-barrel-exports.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-barrel-exports.js +78 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-cn-merge.js +84 -0
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.d.ts +9 -0
- package/dist/eslint/pasika/rules/enforce-cva-variant-props.js +76 -0
- package/dist/eslint/pasika/rules/filename-case.d.ts +2 -0
- package/dist/eslint/pasika/rules/filename-case.js +100 -0
- package/dist/eslint/pasika/rules/import-boundaries.d.ts +2 -0
- package/dist/eslint/pasika/rules/{organization-imports.js → import-boundaries.js} +11 -1
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.d.ts +9 -0
- package/dist/eslint/pasika/rules/no-arbitrary-tailwind.js +107 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.d.ts +9 -0
- package/dist/eslint/pasika/rules/no-mixed-concerns.js +84 -0
- package/package.json +5 -16
- package/claude/hooks/.vulyk +0 -3
- package/claude/hooks/AGENTS.md +0 -3
- package/claude/hooks/CLAUDE.md +0 -1
- package/claude/hooks/claude-hooks.md +0 -30
- package/claude/hooks/notification.sh +0 -38
- package/claude/hooks/protect-files.sh +0 -21
- package/claude/hooks/status-line/index.js +0 -57
- package/claude/scripts/render-settings.ts +0 -223
- package/claude/settings.base.json +0 -38
- package/dist/claude/scripts/render-settings.js +0 -145
- package/dist/eslint/pasika/rules/organization-imports.d.ts +0 -2
- package/dist/eslint.config.js +0 -7
- package/dist/scripts/pasika.js +0 -59
- package/docs/agent-conventions.md +0 -21
- package/docs/claude/hooks.md +0 -30
- package/docs/code-organization-guide/code-organization-guide.md +0 -65
- package/docs/code-organization-guide/references/application-architecture-reference.md +0 -107
- package/docs/code-organization-guide/rules/component-placement-rule.md +0 -135
- package/docs/code-organization-guide/rules/configuration-rule.md +0 -42
- package/docs/code-organization-guide/rules/constants-rule.md +0 -77
- package/docs/code-organization-guide/rules/exports-and-imports-rule.md +0 -81
- package/docs/code-organization-guide/rules/folder-nesting-rule.md +0 -83
- package/docs/code-organization-guide/rules/hook-extraction-rule.md +0 -142
- package/docs/code-organization-guide/rules/interactive-component-rule.md +0 -100
- package/docs/code-organization-guide/rules/jsx-hygiene-rule.md +0 -68
- package/docs/code-organization-guide/rules/locales-rule.md +0 -55
- package/docs/code-organization-guide/rules/nameable-visual-concept-rule.md +0 -66
- package/docs/code-organization-guide/rules/native-prop-forwarding-rule.md +0 -38
- package/docs/code-organization-guide/rules/no-mixed-concerns-rule.md +0 -64
- package/docs/code-organization-guide/rules/repeated-structure-rule.md +0 -93
- package/docs/code-organization-guide/rules/smart-vs-dumb-component-rule.md +0 -116
- package/docs/code-organization-guide/rules/sole-state-owner-rule.md +0 -102
- package/docs/code-organization-guide/rules/types-and-schemas-rule.md +0 -138
- package/docs/code-organization-guide/rules/utilities-rule.md +0 -87
- package/docs/documentation-guide/_templates/guide.md +0 -19
- package/docs/documentation-guide/_templates/reference.md +0 -17
- package/docs/documentation-guide/_templates/rule.md +0 -21
- package/docs/documentation-guide/documentation-guide.md +0 -13
- package/docs/documentation-guide/references/documentation-types-reference.md +0 -9
- package/docs/documentation-guide/rules/guide-creation-rule.md +0 -112
- package/docs/documentation-guide/rules/reference-creation-rule.md +0 -132
- package/docs/documentation-guide/rules/rule-creation-rule.md +0 -81
- package/docs/documentation-guide/rules/template-usage-rule.md +0 -48
- package/docs/shadcn-theme.md +0 -121
- package/docs/styling-guide/rules/class-composition-rule.md +0 -32
- package/docs/styling-guide/rules/color-role-naming-rule.md +0 -115
- package/docs/styling-guide/rules/component-state-rule.md +0 -26
- package/docs/styling-guide/rules/component-variant-rule.md +0 -128
- package/docs/styling-guide/rules/global-style-system-rule.md +0 -63
- package/docs/styling-guide/rules/style-placement-rule.md +0 -49
- package/docs/styling-guide/rules/tailwind-utility-rule.md +0 -48
- package/docs/styling-guide/rules/theme-and-utility-definition-rule.md +0 -129
- package/docs/styling-guide/rules/theme-token-rule.md +0 -32
- package/docs/styling-guide/styling-guide.md +0 -17
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { access, copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { constants } from "node:fs";
|
|
4
|
-
import { fileURLToPath } from "node:url";
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
const isRecord = (value) => typeof value === "object" && value !== null;
|
|
8
|
-
const fileExists = async (filePath) => {
|
|
9
|
-
try {
|
|
10
|
-
await access(filePath, constants.F_OK);
|
|
11
|
-
return true;
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
return false;
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
const isClaudeSettings = (value) => {
|
|
18
|
-
if (!isRecord(value)) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
const hooks = value.hooks;
|
|
22
|
-
const statusLine = value.statusLine;
|
|
23
|
-
if (!isRecord(hooks) || !isRecord(statusLine) || typeof statusLine.command !== "string") {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
const notification = hooks.Notification;
|
|
27
|
-
if (notification !== undefined && !Array.isArray(notification)) {
|
|
28
|
-
return false;
|
|
29
|
-
}
|
|
30
|
-
const preToolUse = hooks.PreToolUse;
|
|
31
|
-
return preToolUse === undefined || Array.isArray(preToolUse);
|
|
32
|
-
};
|
|
33
|
-
const readClaudeSettings = async (templatePath) => {
|
|
34
|
-
const templateRaw = await readFile(templatePath, "utf8");
|
|
35
|
-
const parsed = JSON.parse(templateRaw);
|
|
36
|
-
if (!isClaudeSettings(parsed)) {
|
|
37
|
-
throw new Error("Claude settings template has an invalid shape.");
|
|
38
|
-
}
|
|
39
|
-
return parsed;
|
|
40
|
-
};
|
|
41
|
-
const mergeStringArrays = (left = [], right = []) => Array.from(new Set([...left, ...right]));
|
|
42
|
-
const upsertHookMatcherEntry = (entries, matcher, command) => {
|
|
43
|
-
const nextEntries = [...(entries ?? [])];
|
|
44
|
-
const existingIndex = nextEntries.findIndex((entry) => (entry.matcher ?? "") === matcher);
|
|
45
|
-
const hook = { type: "command", command };
|
|
46
|
-
if (existingIndex === -1) {
|
|
47
|
-
nextEntries.push({ matcher, hooks: [hook] });
|
|
48
|
-
return nextEntries;
|
|
49
|
-
}
|
|
50
|
-
const existingEntry = nextEntries[existingIndex];
|
|
51
|
-
nextEntries[existingIndex] = { ...existingEntry, hooks: [hook] };
|
|
52
|
-
return nextEntries;
|
|
53
|
-
};
|
|
54
|
-
const mergeClaudeSettings = (existingSettings, baseSettings, packagePath) => {
|
|
55
|
-
const notificationCommand = `${packagePath}/hooks/notification.sh`;
|
|
56
|
-
const protectFilesCommand = `${packagePath}/hooks/protect-files.sh`;
|
|
57
|
-
const statusLineCommand = `node ${packagePath}/hooks/status-line/index.js`;
|
|
58
|
-
return {
|
|
59
|
-
...existingSettings,
|
|
60
|
-
$schema: baseSettings.$schema ?? existingSettings.$schema,
|
|
61
|
-
attribution: { ...(baseSettings.attribution ?? {}), ...(existingSettings.attribution ?? {}) },
|
|
62
|
-
permissions: {
|
|
63
|
-
...(existingSettings.permissions ?? {}),
|
|
64
|
-
...(baseSettings.permissions ?? {}),
|
|
65
|
-
ask: mergeStringArrays(baseSettings.permissions?.ask, existingSettings.permissions?.ask),
|
|
66
|
-
deny: mergeStringArrays(baseSettings.permissions?.deny, existingSettings.permissions?.deny),
|
|
67
|
-
},
|
|
68
|
-
hooks: {
|
|
69
|
-
...existingSettings.hooks,
|
|
70
|
-
Notification: upsertHookMatcherEntry(existingSettings.hooks.Notification, "", notificationCommand),
|
|
71
|
-
PreToolUse: upsertHookMatcherEntry(existingSettings.hooks.PreToolUse, "Edit|Write", protectFilesCommand),
|
|
72
|
-
},
|
|
73
|
-
statusLine: {
|
|
74
|
-
...existingSettings.statusLine,
|
|
75
|
-
...baseSettings.statusLine,
|
|
76
|
-
type: "command",
|
|
77
|
-
command: statusLineCommand,
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
};
|
|
81
|
-
export const renderClaude = async (targetDir, force, packageRoot) => {
|
|
82
|
-
const templatePath = path.join(packageRoot, "claude/settings.base.json");
|
|
83
|
-
const baseSettings = await readClaudeSettings(templatePath);
|
|
84
|
-
const packagePath = "./node_modules/pasika/claude";
|
|
85
|
-
const notificationEntries = upsertHookMatcherEntry(baseSettings.hooks.Notification, "", `${packagePath}/hooks/notification.sh`);
|
|
86
|
-
const preToolUseEntries = upsertHookMatcherEntry(baseSettings.hooks.PreToolUse, "Edit|Write", `${packagePath}/hooks/protect-files.sh`);
|
|
87
|
-
const settings = {
|
|
88
|
-
...baseSettings,
|
|
89
|
-
hooks: {
|
|
90
|
-
...baseSettings.hooks,
|
|
91
|
-
Notification: notificationEntries,
|
|
92
|
-
PreToolUse: preToolUseEntries,
|
|
93
|
-
},
|
|
94
|
-
statusLine: {
|
|
95
|
-
...baseSettings.statusLine,
|
|
96
|
-
type: "command",
|
|
97
|
-
command: `node ${packagePath}/hooks/status-line/index.js`,
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
const outputDir = path.join(targetDir, ".claude");
|
|
101
|
-
const outputPath = path.join(outputDir, "settings.json");
|
|
102
|
-
const hooksOutputDir = path.join(outputDir, "hooks");
|
|
103
|
-
await mkdir(outputDir, { recursive: true });
|
|
104
|
-
await mkdir(hooksOutputDir, { recursive: true });
|
|
105
|
-
const finalSettings = !force && (await fileExists(outputPath))
|
|
106
|
-
? mergeClaudeSettings(await readClaudeSettings(outputPath), settings, packagePath)
|
|
107
|
-
: settings;
|
|
108
|
-
await writeFile(outputPath, `${JSON.stringify(finalSettings, null, 2)}\n`, "utf8");
|
|
109
|
-
const outputs = [outputPath];
|
|
110
|
-
const hookDocFiles = ["AGENTS.md", "CLAUDE.md"];
|
|
111
|
-
for (const fileName of hookDocFiles) {
|
|
112
|
-
const sourcePath = path.join(packageRoot, "claude/hooks", fileName);
|
|
113
|
-
if (!(await fileExists(sourcePath))) {
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
const destinationPath = path.join(hooksOutputDir, fileName);
|
|
117
|
-
await copyFile(sourcePath, destinationPath);
|
|
118
|
-
outputs.push(destinationPath);
|
|
119
|
-
}
|
|
120
|
-
return outputs;
|
|
121
|
-
};
|
|
122
|
-
const isDirectRun = process.argv[1] ? path.resolve(process.argv[1]) === __filename : false;
|
|
123
|
-
if (isDirectRun) {
|
|
124
|
-
const args = process.argv.slice(2);
|
|
125
|
-
const getArgValue = (flag) => {
|
|
126
|
-
const index = args.indexOf(flag);
|
|
127
|
-
if (index === -1) {
|
|
128
|
-
return undefined;
|
|
129
|
-
}
|
|
130
|
-
return args[index + 1];
|
|
131
|
-
};
|
|
132
|
-
const targetDir = path.resolve(process.cwd(), getArgValue("--target-dir") ?? ".");
|
|
133
|
-
const force = args.includes("--force");
|
|
134
|
-
const packageRoot = path.resolve(__dirname, "../..");
|
|
135
|
-
try {
|
|
136
|
-
const outputs = await renderClaude(targetDir, force, packageRoot);
|
|
137
|
-
for (const output of outputs) {
|
|
138
|
-
process.stdout.write(`Wrote ${output}\n`);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
143
|
-
process.exit(1);
|
|
144
|
-
}
|
|
145
|
-
}
|
package/dist/eslint.config.js
DELETED
package/dist/scripts/pasika.js
DELETED
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
import { renderClaude } from "../claude/scripts/render-settings.js";
|
|
5
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
6
|
-
const __dirname = path.dirname(__filename);
|
|
7
|
-
const packageRoot = path.basename(path.dirname(__dirname)) === "dist" ? path.resolve(__dirname, "../..") : path.resolve(__dirname, "..");
|
|
8
|
-
const args = process.argv.slice(2);
|
|
9
|
-
const helpText = `pasika
|
|
10
|
-
|
|
11
|
-
Usage:
|
|
12
|
-
pasika claude [--target-dir <path>] [--force]
|
|
13
|
-
|
|
14
|
-
Platforms:
|
|
15
|
-
claude
|
|
16
|
-
|
|
17
|
-
Examples:
|
|
18
|
-
npx pasika claude
|
|
19
|
-
npx pasika claude --force
|
|
20
|
-
|
|
21
|
-
Behavior:
|
|
22
|
-
merges into existing .claude/settings.json by default
|
|
23
|
-
replaces it only when --force is passed
|
|
24
|
-
`;
|
|
25
|
-
const getFlagValue = (flag) => {
|
|
26
|
-
const index = args.indexOf(flag);
|
|
27
|
-
if (index === -1) {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
return args[index + 1];
|
|
31
|
-
};
|
|
32
|
-
const hasFlag = (flag) => args.includes(flag);
|
|
33
|
-
const platform = args[0];
|
|
34
|
-
if (!platform || hasFlag("--help") || hasFlag("-h")) {
|
|
35
|
-
process.stdout.write(helpText);
|
|
36
|
-
process.exit(platform ? 0 : 1);
|
|
37
|
-
}
|
|
38
|
-
const targetDir = path.resolve(process.cwd(), getFlagValue("--target-dir") ?? ".");
|
|
39
|
-
const force = hasFlag("--force");
|
|
40
|
-
const run = async () => {
|
|
41
|
-
switch (platform) {
|
|
42
|
-
case "claude":
|
|
43
|
-
return renderClaude(targetDir, force, packageRoot);
|
|
44
|
-
default:
|
|
45
|
-
throw new Error(`Unknown platform: ${platform}`);
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
try {
|
|
49
|
-
const outputs = await run();
|
|
50
|
-
process.stdout.write(`Initialized ${platform} in ${targetDir}\n`);
|
|
51
|
-
for (const output of outputs) {
|
|
52
|
-
process.stdout.write(`Wrote ${output}\n`);
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
catch (error) {
|
|
56
|
-
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
57
|
-
process.stderr.write(helpText);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
# Agent Conventions
|
|
2
|
-
|
|
3
|
-
Repo-wide engineering rules and commit hygiene for AI agents.
|
|
4
|
-
|
|
5
|
-
## Data Contracts
|
|
6
|
-
|
|
7
|
-
- Use Zod schemas for runtime validation; prioritize them over ad hoc type guards.
|
|
8
|
-
- Prefer strict-by-default data contracts. Make fields required unless real runtime evidence shows they must be nullable or optional.
|
|
9
|
-
- Do not preserve backward compatibility by default. Prefer simplifying and updating all in-repo call sites unless the user explicitly asks for compatibility.
|
|
10
|
-
|
|
11
|
-
## Code Quality
|
|
12
|
-
|
|
13
|
-
- Never use `eslint-disable` directives. Fix issues properly.
|
|
14
|
-
- Never commit with the `--no-verify` flag.
|
|
15
|
-
|
|
16
|
-
## Vulyk
|
|
17
|
-
|
|
18
|
-
- Make Vulyk registry changes with Vulyk commands, not by editing generated files.
|
|
19
|
-
- Treat `AGENTS.md`, `docs/external`, `.agents/skills`, and `.vulyk` as generated output.
|
|
20
|
-
- Use ephemeral commands such as `npx vulyk@latest ...`.
|
|
21
|
-
- Do not add Vulyk to `package.json` only to run the CLI.
|
package/docs/claude/hooks.md
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# Claude Hooks
|
|
2
|
-
|
|
3
|
-
`pasika` ships the reusable shared Claude hooks.
|
|
4
|
-
|
|
5
|
-
## `status-line/index.js`
|
|
6
|
-
|
|
7
|
-
Displays a two-line status view with:
|
|
8
|
-
|
|
9
|
-
- model name
|
|
10
|
-
- current folder
|
|
11
|
-
- git branch when available
|
|
12
|
-
- used context percentage as a 10-segment bar
|
|
13
|
-
- session cost
|
|
14
|
-
- session duration
|
|
15
|
-
|
|
16
|
-
## `notification.sh`
|
|
17
|
-
|
|
18
|
-
Sends a desktop notification when Claude needs attention.
|
|
19
|
-
|
|
20
|
-
- macOS: `terminal-notifier` with click-to-focus VS Code behavior, falls back to `osascript`
|
|
21
|
-
- Linux: `notify-send`
|
|
22
|
-
- Windows: PowerShell message box
|
|
23
|
-
|
|
24
|
-
## `protect-files.sh`
|
|
25
|
-
|
|
26
|
-
Blocks edits to:
|
|
27
|
-
|
|
28
|
-
- `.env` files
|
|
29
|
-
- `package-lock.json`
|
|
30
|
-
- files inside `.git/`
|
|
@@ -1,65 +0,0 @@
|
|
|
1
|
-
# Code Organization Guide
|
|
2
|
-
|
|
3
|
-
This guide covers how to organize code in the repository's `src/` tree — placement, extraction, and module conventions — so structure stays consistent across contributors and reviewers.
|
|
4
|
-
|
|
5
|
-
For every JavaScript or TypeScript module covered by this guide, also follow the [Exports and Imports Rule](rules/exports-and-imports-rule.md).
|
|
6
|
-
|
|
7
|
-
## How To Organize a Component
|
|
8
|
-
|
|
9
|
-
Use this no matter whether you are adding a new component or extracting from existing code; the steps below apply in any case.
|
|
10
|
-
|
|
11
|
-
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
12
|
-
2. Pick the component's placement per the [Component Placement Rule](rules/component-placement-rule.md).
|
|
13
|
-
3. Classify the component as smart or dumb per the [Smart vs Dumb Component Rule](rules/smart-vs-dumb-component-rule.md).
|
|
14
|
-
4. Follow the [Native Prop Forwarding Rule](rules/native-prop-forwarding-rule.md) when a `src/shared/` component renders the same intrinsic root element in every rendered result, so its native prop contract remains available.
|
|
15
|
-
5. Keep each component file to exactly one React component per the [No Mixed Concerns Rule](rules/no-mixed-concerns-rule.md).
|
|
16
|
-
6. Extract interactive elements per the [Interactive Component Rule](rules/interactive-component-rule.md).
|
|
17
|
-
7. Extract sole-state-owner blocks per the [Sole State Owner Rule](rules/sole-state-owner-rule.md).
|
|
18
|
-
8. Extract repeated structures per the [Repeated Structure Rule](rules/repeated-structure-rule.md).
|
|
19
|
-
9. Extract a group of elements when one clear component name describes it per the [Nameable Visual Concept Rule](rules/nameable-visual-concept-rule.md).
|
|
20
|
-
10. For every component extracted in steps 6–9, repeat steps 2–5.
|
|
21
|
-
11. Repeat steps 6–9 for an extracted component only when its JSX has an applicable trigger; stop when no new component is required.
|
|
22
|
-
12. Nest a component when it gains exclusive children per the [Folder Nesting Rule](rules/folder-nesting-rule.md).
|
|
23
|
-
13. Keep the component's JSX clean per the [JSX Hygiene Rule](rules/jsx-hygiene-rule.md).
|
|
24
|
-
|
|
25
|
-
## How To Organize a Type or Schema
|
|
26
|
-
|
|
27
|
-
Use this when adding or moving a type or schema.
|
|
28
|
-
|
|
29
|
-
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
30
|
-
2. Follow the [Types and Schemas Rule](rules/types-and-schemas-rule.md).
|
|
31
|
-
|
|
32
|
-
## How To Organize a Constant
|
|
33
|
-
|
|
34
|
-
Use this when adding or moving a constant.
|
|
35
|
-
|
|
36
|
-
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
37
|
-
2. Follow the [Constants Rule](rules/constants-rule.md).
|
|
38
|
-
|
|
39
|
-
## How To Organize a Utility
|
|
40
|
-
|
|
41
|
-
Use this when adding, extracting, or moving a pure function.
|
|
42
|
-
|
|
43
|
-
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
44
|
-
2. Follow the [Utilities Rule](rules/utilities-rule.md).
|
|
45
|
-
|
|
46
|
-
## How To Organize Configuration
|
|
47
|
-
|
|
48
|
-
Use this when adding or moving an application configuration object.
|
|
49
|
-
|
|
50
|
-
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
51
|
-
2. Follow the [Configuration Rule](rules/configuration-rule.md).
|
|
52
|
-
|
|
53
|
-
## How To Organize a Custom Hook
|
|
54
|
-
|
|
55
|
-
Use this when extracting or moving a custom hook.
|
|
56
|
-
|
|
57
|
-
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
58
|
-
2. Follow the [Hook Extraction Rule](rules/hook-extraction-rule.md).
|
|
59
|
-
|
|
60
|
-
## How To Organize a Locale String
|
|
61
|
-
|
|
62
|
-
Use this when adding or moving user-facing text.
|
|
63
|
-
|
|
64
|
-
1. Read the [Application Architecture Reference](references/application-architecture-reference.md) to choose the right layer and scope.
|
|
65
|
-
2. Follow the [Locales Rule](rules/locales-rule.md).
|
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
# Application Architecture Reference
|
|
2
|
-
|
|
3
|
-
Use this reference to look up the canonical shape of the repository's `src/` tree and its Layer Model.
|
|
4
|
-
|
|
5
|
-
## Layer Model
|
|
6
|
-
|
|
7
|
-
This section describes the repository's four layers, root support scope, and dependency direction. Use it to decide where a component or support file belongs and which layers it can import from.
|
|
8
|
-
|
|
9
|
-
| Layer or scope | Path | Permitted contents | Imports from |
|
|
10
|
-
| -------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------ |
|
|
11
|
-
| `app` | `src/app/` | Next.js App Router framework-convention files and assets, plus styles required by routing files | compositions, features, shared, root |
|
|
12
|
-
| `compositions` | `src/compositions/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`) | compositions, features, shared, root |
|
|
13
|
-
| `features` | `src/features/<feature>/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`). `src/features/` itself holds feature folders only — never components or support folders | same feature, shared, root |
|
|
14
|
-
| `shared` | `src/shared/` | Components and support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`) | shared, root |
|
|
15
|
-
| `root` | `src/` | App-wide support files (`hooks/`, `types/`, `schemas/`, `constants/`, `utils/`), `locales/`, and `config/` | root support folders; a configuration module may also import files in its own `src/config/<config-name>/` directory |
|
|
16
|
-
|
|
17
|
-
Root support folders are `hooks/`, `types/`, `schemas/`, `constants/`, `utils/`, `config/`, and `locales/`.
|
|
18
|
-
|
|
19
|
-
## Closest Common Folder (CCF)
|
|
20
|
-
|
|
21
|
-
The CCF is the deepest eligible folder that contains every consumer of an item.
|
|
22
|
-
|
|
23
|
-
- Eligible folders are a nested component directory, a feature folder, a configuration-object directory, `src/features/`, `src/compositions/`, `src/shared/`, and `src/`.
|
|
24
|
-
- Support folders and `src/app/` do not determine the CCF. A consumer in a support folder counts at its enclosing eligible component, feature, composition, shared, configuration-object, or root scope. A Next.js route-file consumer counts at `src/` unless a rule explicitly excludes it. `src/features/` is a non-placeable CCF boundary; the relevant placement rule maps it to a destination.
|
|
25
|
-
- A consumer is a module under `src/` that uses the item. For initial placement only, include the origin module that contains the item before extraction; omit it after extraction. Apply a rule's import exclusions before calculating the CCF, then use it for placement unless that rule states otherwise.
|
|
26
|
-
|
|
27
|
-
## Application Structure
|
|
28
|
-
|
|
29
|
-
This tree shows the canonical directory layout and naming conventions for files and folders inside `src/`. It is included so readers can verify their structure matches the project standard at a glance.
|
|
30
|
-
|
|
31
|
-
```
|
|
32
|
-
src/
|
|
33
|
-
├── app/ # Next.js framework-convention files, assets, and route styles
|
|
34
|
-
│ ├── globals.css # Global stylesheet imported by the root layout
|
|
35
|
-
│ ├── layout.tsx # Root layout
|
|
36
|
-
│ └── <route>/ # Next.js App Router routing files
|
|
37
|
-
├── compositions/ # Components stay flat siblings by default
|
|
38
|
-
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
39
|
-
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
40
|
-
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
41
|
-
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
42
|
-
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
43
|
-
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
44
|
-
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
45
|
-
│ │ ├── hooks/ # Nested-scoped support folders
|
|
46
|
-
│ │ ├── types/
|
|
47
|
-
│ │ ├── schemas/
|
|
48
|
-
│ │ ├── constants/
|
|
49
|
-
│ │ └── utils/
|
|
50
|
-
│ ├── hooks/ # Composition-scoped custom hooks
|
|
51
|
-
│ ├── types/ # Composition-scoped TypeScript types
|
|
52
|
-
│ ├── schemas/ # Composition-scoped validation schemas
|
|
53
|
-
│ ├── constants/ # Composition-scoped constants
|
|
54
|
-
│ └── utils/ # Composition-scoped pure functions
|
|
55
|
-
├── features/ # Feature folders only — no components, no support folders here
|
|
56
|
-
│ └── <feature>/ # One folder per feature — a feature can hold several components
|
|
57
|
-
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
58
|
-
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
59
|
-
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
60
|
-
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
61
|
-
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
62
|
-
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
63
|
-
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
64
|
-
│ │ ├── hooks/ # Nested-scoped support folders
|
|
65
|
-
│ │ ├── types/
|
|
66
|
-
│ │ ├── schemas/
|
|
67
|
-
│ │ ├── constants/
|
|
68
|
-
│ │ └── utils/
|
|
69
|
-
│ ├── hooks/ # Feature-scoped custom hooks
|
|
70
|
-
│ ├── types/ # Feature-scoped TypeScript types
|
|
71
|
-
│ ├── schemas/ # Feature-scoped validation schemas
|
|
72
|
-
│ ├── constants/ # Feature-scoped constants
|
|
73
|
-
│ └── utils/ # Feature-scoped pure functions
|
|
74
|
-
├── shared/ # Components stay flat siblings by default
|
|
75
|
-
│ ├── <ComponentA>.tsx # Smart component (PascalCase-named)
|
|
76
|
-
│ ├── <component-b>.tsx # Dumb component (kebab-case-named)
|
|
77
|
-
│ ├── <component-part>.tsx # Dumb component — shared by A and B, so it stays flat too
|
|
78
|
-
│ ├── <NestedComponent>/ # Smart (PascalCase-named) — or kebab-case-named for dumb; nested only once it gains exclusive children
|
|
79
|
-
│ │ ├── index.ts # Named-re-exports only the nested parent
|
|
80
|
-
│ │ ├── <NestedComponent>.tsx # Smart (PascalCase-named) — or kebab-case-named for dumb
|
|
81
|
-
│ │ ├── <nested-part>.tsx # Exclusive child — not in index.ts
|
|
82
|
-
│ │ ├── hooks/ # Nested-scoped support folders
|
|
83
|
-
│ │ ├── types/
|
|
84
|
-
│ │ ├── schemas/
|
|
85
|
-
│ │ ├── constants/
|
|
86
|
-
│ │ └── utils/
|
|
87
|
-
│ ├── hooks/ # Shared-scoped custom hooks
|
|
88
|
-
│ ├── types/ # Shared-scoped TypeScript types
|
|
89
|
-
│ ├── schemas/ # Shared-scoped validation schemas
|
|
90
|
-
│ ├── constants/ # Shared-scoped constants
|
|
91
|
-
│ └── utils/ # Shared-scoped pure functions
|
|
92
|
-
├── hooks/ # App-wide custom hooks
|
|
93
|
-
├── types/ # App-wide TypeScript types
|
|
94
|
-
├── schemas/ # App-wide validation schemas
|
|
95
|
-
├── constants/ # App-wide constants
|
|
96
|
-
├── utils/ # App-wide pure functions
|
|
97
|
-
├── config/ # Configuration objects that centralize application behavior
|
|
98
|
-
│ └── <config-name>/ # One folder per configuration object
|
|
99
|
-
│ ├── index.ts # Config object
|
|
100
|
-
│ ├── hooks/ # Config-only custom hooks
|
|
101
|
-
│ ├── types/ # Config-only TypeScript types
|
|
102
|
-
│ ├── schemas/ # Config-only validation schemas
|
|
103
|
-
│ ├── constants/ # Config-only constants
|
|
104
|
-
│ └── utils/ # Config-only pure functions
|
|
105
|
-
└── locales/ # App-wide locale strings
|
|
106
|
-
└── index.ts # Single locale-registration file
|
|
107
|
-
```
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
# Component Placement Rule
|
|
2
|
-
|
|
3
|
-
Without clear placement, it is hard to tell where a component belongs and reuse can create tangled dependencies. This rule gives each component a specific place in the application structure.
|
|
4
|
-
|
|
5
|
-
- A routing UI file is a Next.js App Router UI file, excluding request handlers and metadata files. A Next.js `layout.tsx` routing UI file MAY render the layout required by its route segment and `children`. Any other routing UI file MAY import root support modules and directly render one feature component, one composition, or one shared component, but MUST NOT own state, callbacks, effects, or page layout.
|
|
6
|
-
- A routing UI file other than `layout.tsx` that needs state, callbacks, effects, or page layout MUST delegate that work to a component in `src/compositions/`.
|
|
7
|
-
- Any other component that imports from two or more feature folders MUST live in `src/compositions/` and is, by definition, a composition.
|
|
8
|
-
- A component whose only consumers are routing files and that does not import from two or more feature folders MUST live in the feature folder whose behavior it implements. A component implements a feature when it reads that feature's data or exposes that feature's actions. If it implements no feature and only one routing file imports it, it MUST introduce a new feature folder for that route; if routing files for two or more routes import it, it MUST live in `src/shared/`. This rule does not apply to Next.js routing files themselves.
|
|
9
|
-
- A component with at least one consumer other than a routing file MUST live in the closest common folder (CCF) of its non-routing consumers.
|
|
10
|
-
- A new component with no consumers and fewer than two feature-folder imports MUST start in the existing feature folder it belongs to, or in a new feature folder when it introduces a new feature. A new component with no feature-specific behavior or data MAY start in `src/shared/` only when it imports no feature folders; otherwise it starts in the feature it imports from.
|
|
11
|
-
- Imports from routing files in `src/app/` MUST NOT count as component consumers or affect CCF calculation.
|
|
12
|
-
- Imports from configuration modules MUST NOT count as component consumers or affect CCF calculation.
|
|
13
|
-
- When a component is imported both inside and outside `src/compositions/`, its CCF MUST be calculated only from imports outside `src/compositions/`.
|
|
14
|
-
- When a component is imported only in `src/compositions/`, its CCF MUST be calculated from those imports.
|
|
15
|
-
- When a component’s CCF is `src/features/`, it MUST live in `src/shared/`.
|
|
16
|
-
- When a component’s CCF is `src/`, it MUST live in `src/shared/`.
|
|
17
|
-
- When a component’s CCF is a nested component directory, it MUST start flat in that directory unless the Folder Nesting Rule requires nesting.
|
|
18
|
-
- When a component’s CCF is a feature folder, `src/compositions/`, or `src/shared/`, it MUST start flat in that folder unless the Folder Nesting Rule requires nesting.
|
|
19
|
-
- `src/app/` MUST contain [Next.js App Router framework-convention files and assets](https://nextjs.org/docs/app/getting-started/project-structure#routing-files), plus styles required by routing files, but MUST NOT contain ordinary components or support folders.
|
|
20
|
-
- A JavaScript or TypeScript module under `src/app/` MUST NOT import modules under `src/` except from `src/compositions/`, `src/features/`, `src/shared/`, and root support folders.
|
|
21
|
-
- A JavaScript or TypeScript module under `src/compositions/` MUST NOT import modules under `src/` except from `src/compositions/`, feature folders, `src/shared/`, and root support folders.
|
|
22
|
-
- A JavaScript or TypeScript module in a feature folder MUST NOT import modules under `src/` except from the same feature folder, `src/shared/`, and root support folders.
|
|
23
|
-
- A JavaScript or TypeScript module under `src/shared/` MUST NOT import modules under `src/` except from `src/shared/` and root support folders.
|
|
24
|
-
- A JavaScript or TypeScript module in a root support folder MUST NOT import modules under `src/` except from root support folders. Any module under `src/config/<config-name>/` MAY also import files under that same configuration-object directory.
|
|
25
|
-
- ESLint MUST enforce these import restrictions for JavaScript and TypeScript modules under `src/`.
|
|
26
|
-
|
|
27
|
-
## Incorrect — Cross-Feature Component Duplicated
|
|
28
|
-
|
|
29
|
-
```text
|
|
30
|
-
src/features/
|
|
31
|
-
├── donation/
|
|
32
|
-
│ ├── DonationCard.tsx
|
|
33
|
-
│ └── status-badge.tsx
|
|
34
|
-
└── stream/
|
|
35
|
-
├── StreamCard.tsx
|
|
36
|
-
└── status-badge.tsx
|
|
37
|
-
```
|
|
38
|
-
|
|
39
|
-
Why: two feature folders contain duplicate copies because neither feature may import from the other.
|
|
40
|
-
|
|
41
|
-
## Correct — Cross-Feature Component in `src/shared/`
|
|
42
|
-
|
|
43
|
-
```text
|
|
44
|
-
src/
|
|
45
|
-
├── features/
|
|
46
|
-
│ ├── donation/
|
|
47
|
-
│ │ └── DonationCard.tsx
|
|
48
|
-
│ └── stream/
|
|
49
|
-
│ └── StreamCard.tsx
|
|
50
|
-
└── shared/
|
|
51
|
-
└── status-badge.tsx
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
Why: the component's CCF is `src/features/`, so it lives in `src/shared/`.
|
|
55
|
-
|
|
56
|
-
## Incorrect — Multi-Feature Composition Inside One Feature
|
|
57
|
-
|
|
58
|
-
```tsx
|
|
59
|
-
// src/features/billing/billing-aggregate-view.tsx
|
|
60
|
-
import { BillingPanel } from "./BillingPanel";
|
|
61
|
-
import { HomeBanner } from "@/features/home/HomeBanner";
|
|
62
|
-
```
|
|
63
|
-
|
|
64
|
-
Why: the component imports from two feature folders, so it cannot live in either feature folder.
|
|
65
|
-
|
|
66
|
-
## Correct — Multi-Feature Component in `src/compositions/`
|
|
67
|
-
|
|
68
|
-
```tsx
|
|
69
|
-
// src/compositions/billing-aggregate-view.tsx
|
|
70
|
-
import { BillingPanel } from "@/features/billing/BillingPanel";
|
|
71
|
-
import { HomeBanner } from "@/features/home/HomeBanner";
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Why: the component imports from two feature folders, so it lives in `src/compositions/`.
|
|
75
|
-
|
|
76
|
-
## Incorrect — Ordinary Component Under `src/app/`
|
|
77
|
-
|
|
78
|
-
```text
|
|
79
|
-
src/app/contact/
|
|
80
|
-
├── _components/
|
|
81
|
-
│ └── contact-page-content.tsx
|
|
82
|
-
└── page.tsx
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
Why: the route folder contains ordinary component structure even though `src/app/` is reserved for framework routing files.
|
|
86
|
-
|
|
87
|
-
## Correct — Thin Route Imports a Page Composition
|
|
88
|
-
|
|
89
|
-
```text
|
|
90
|
-
src/
|
|
91
|
-
├── app/contact/
|
|
92
|
-
│ └── page.tsx
|
|
93
|
-
├── compositions/
|
|
94
|
-
│ └── contact-page-content.tsx
|
|
95
|
-
└── features/
|
|
96
|
-
├── contact/
|
|
97
|
-
│ └── contact-information.tsx
|
|
98
|
-
└── location/
|
|
99
|
-
└── office-location.tsx
|
|
100
|
-
```
|
|
101
|
-
|
|
102
|
-
```tsx
|
|
103
|
-
// src/compositions/contact-page-content.tsx
|
|
104
|
-
import { ContactInformation } from "@/features/contact/contact-information";
|
|
105
|
-
import { OfficeLocation } from "@/features/location/office-location";
|
|
106
|
-
import { locales } from "@/locales";
|
|
107
|
-
|
|
108
|
-
export function ContactPageContent(): React.JSX.Element {
|
|
109
|
-
return (
|
|
110
|
-
<main data-testid="contact-page-content">
|
|
111
|
-
<header>
|
|
112
|
-
<h1>{locales.contactPageTitle}</h1>
|
|
113
|
-
<p>{locales.contactPageDescription}</p>
|
|
114
|
-
</header>
|
|
115
|
-
<section>
|
|
116
|
-
<h2>{locales.contactInformationHeading}</h2>
|
|
117
|
-
<ContactInformation />
|
|
118
|
-
</section>
|
|
119
|
-
<section>
|
|
120
|
-
<h2>{locales.officeLocationHeading}</h2>
|
|
121
|
-
<OfficeLocation />
|
|
122
|
-
</section>
|
|
123
|
-
</main>
|
|
124
|
-
);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// src/app/contact/page.tsx
|
|
128
|
-
import { ContactPageContent } from "@/compositions/contact-page-content";
|
|
129
|
-
|
|
130
|
-
export default function Page(): React.JSX.Element {
|
|
131
|
-
return <ContactPageContent />;
|
|
132
|
-
}
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Why: the routing file remains thin while the page composition provides the contact page layout and combines components from two feature folders.
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
# Configuration Rule
|
|
2
|
-
|
|
3
|
-
Configuration objects centralize values that control application behavior. This rule keeps each object and the files that only support it together in `src/config/`.
|
|
4
|
-
|
|
5
|
-
- A configuration object is an app-wide object that selects or parameterizes application behavior. Feature options and domain values are not configuration objects.
|
|
6
|
-
- All configuration objects MUST live in `src/config/`.
|
|
7
|
-
- A configuration object MUST use `src/config/<config-name>/index.ts`.
|
|
8
|
-
- A configuration support file is a hook, type, schema, constant, or utility used only to implement one configuration object; config-only extraction applies even with one consumer.
|
|
9
|
-
- A configuration support file MUST live in its matching dedicated folder under `src/config/<config-name>/`.
|
|
10
|
-
- A configuration support file remains in that configuration-object directory while every consumer is in that directory; it MUST move to its matching root support folder at the root CCF when any consumer is outside it.
|
|
11
|
-
|
|
12
|
-
## Incorrect — Supporting Schema Outside Its Config Folder
|
|
13
|
-
|
|
14
|
-
```text
|
|
15
|
-
src/config/
|
|
16
|
-
├── home-feed/
|
|
17
|
-
│ └── index.ts
|
|
18
|
-
└── home-feed-schema.ts
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
Why: the schema sits outside the `home-feed/` configuration folder.
|
|
22
|
-
|
|
23
|
-
## Correct — Configuration Object with Its Supporting Schema
|
|
24
|
-
|
|
25
|
-
```text
|
|
26
|
-
src/config/
|
|
27
|
-
└── home-feed/
|
|
28
|
-
├── index.ts
|
|
29
|
-
└── schemas/
|
|
30
|
-
└── index.ts
|
|
31
|
-
```
|
|
32
|
-
|
|
33
|
-
```ts
|
|
34
|
-
// src/config/home-feed/index.ts
|
|
35
|
-
import { homeFeedConfigSchema } from "./schemas";
|
|
36
|
-
|
|
37
|
-
export const homeFeedConfig = homeFeedConfigSchema.parse({
|
|
38
|
-
freshVideoMaxAgeDays: 7,
|
|
39
|
-
});
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Why: the configuration object and its supporting schema are grouped in the same configuration folder.
|