create-minlang-app 0.1.0 → 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.mjs CHANGED
@@ -4,7 +4,7 @@
4
4
  // published @minlang/* runtime packages. Compile with ml1 (see the printed
5
5
  // next steps), run with pnpm, deploy via the included Vercel workflow.
6
6
 
7
- import { cpSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
7
+ import { cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync } from "node:fs";
8
8
  import { dirname, join } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
10
 
@@ -37,22 +37,44 @@ substitute(dest);
37
37
  renameSync(join(dest, "__APP_NAME__.ml"), join(dest, `${name}.ml`));
38
38
  // npm strips dotfiles from published packages; restore them.
39
39
  renameSync(join(dest, "gitignore"), join(dest, ".gitignore"));
40
+ renameSync(join(dest, "gitattributes"), join(dest, ".gitattributes"));
40
41
  renameSync(join(dest, "github"), join(dest, ".github"));
41
42
 
43
+ // .github/ only works at the repository root: warn when the scaffold is
44
+ // nested inside an existing git repo, where the deploy workflow would be dead.
45
+ const enclosingGitRoot = (start) => {
46
+ for (let dir = start; ; dir = dirname(dir)) {
47
+ if (existsSync(join(dir, ".git"))) return dir;
48
+ if (dirname(dir) === dir) return null;
49
+ }
50
+ };
51
+ const gitRoot = enclosingGitRoot(dirname(dest));
52
+
42
53
  console.log(`
43
54
  Created ${name}/ — a MinLang web app.
44
55
 
45
56
  Next steps:
46
57
  cd ${name}
47
- # 1. install the ml1 compiler (pick one):
48
- # brew install codeshift-ai-solutions/tap/ml1
49
- # bash <(curl -fsSL https://raw.githubusercontent.com/codeshift-ai-solutions/minlang-core/main/install/install.sh)
50
- # 2. compile, install, run:
58
+ # 1. install the ml1 compiler:
59
+ # bash <(curl -fsSL https://raw.githubusercontent.com/codeshift-ai-solutions/minlang-releases/main/install/install.sh)
60
+ # (Windows: iwr -useb https://raw.githubusercontent.com/codeshift-ai-solutions/minlang-releases/main/install/install.ps1 | iex)
61
+ # 2. one-time toolchain setup:
62
+ corepack enable && corepack prepare pnpm@10.33.0 --activate
63
+ # 3. compile, install, run:
51
64
  make compile
52
65
  pnpm --dir app install
53
- pnpm --dir app dev
66
+ make dev # http://localhost:3111
54
67
 
55
68
  Your whole app lives in ${name}.ml (it starts as a task tracker — replace it).
56
- Authoring guide: https://github.com/codeshift-ai-solutions/minlang-core/blob/main/docs/ai/language/WEB_APP_GUIDE.md
57
- Deploys: push to GitHub with org secrets VERCEL_TOKEN/VERCEL_ORG_ID set.
69
+ Agent instructions: ${name}/AGENTS.md
70
+ Language rules: https://github.com/codeshift-ai-solutions/minlang-releases/releases/latest/download/minlang-language-bundle.md
71
+ Deploys: push to GitHub with org secrets VERCEL_TOKEN/VERCEL_ORG_ID set;
72
+ the first deploy prints a VERCEL_PROJECT_ID to pin as a repo secret.
58
73
  `);
74
+
75
+ if (gitRoot) {
76
+ console.warn(`warning: ${name}/ is nested inside the git repository at ${gitRoot}.
77
+ GitHub only runs workflows from the repository root, so ${name}/.github/ would
78
+ be ignored. Either create the app in its own repository (recommended) or move
79
+ ${name}/.github/workflows/* to ${gitRoot}/.github/workflows/ and adjust paths.`);
80
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-minlang-app",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Scaffold a MinLang web app: write one .ml file, compile with ml1, deploy.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -0,0 +1,66 @@
1
+ # Agent instructions for __APP_NAME__
2
+
3
+ This is a **MinLang** app. The entire application is `__APP_NAME__.ml` —
4
+ entities, constraints, actions, queries, screens, and tests in one file.
5
+ Everything under `app/generated/` is compiler output: committed, **never
6
+ hand-edited**. The `app/` directory is a thin Next.js shell over the
7
+ published `@minlang/*` runtime packages; it rarely needs changes.
8
+
9
+ ## The one rule
10
+
11
+ To change behavior, edit `__APP_NAME__.ml`, then run `make compile`.
12
+ Never edit files under `app/generated/` — CI recompiles with `--check`
13
+ and fails the build if they drift from the `.ml` source.
14
+
15
+ ## Language authority (fetch before writing MinLang)
16
+
17
+ Download the single-file language bundle and read it before producing or
18
+ modifying any MinLang source:
19
+
20
+ https://github.com/codeshift-ai-solutions/minlang-releases/releases/latest/download/minlang-language-bundle.md
21
+
22
+ It contains the canonical rules (Creator + Gatekeeper packets), the
23
+ detector list, and the end-to-end web-app authoring guide. Highlights:
24
+ - Forbidden everywhere: `now()`, `today()`, `random()`, `current_user`
25
+ (runtime inputs are injected: `created_at`, `current_time`, `actor_id`,
26
+ `rng_draw`, `request_id`).
27
+ - Generate -> validate -> fix -> revalidate until zero violations, then
28
+ output. `ml1 validate` is the same gate CI runs.
29
+
30
+ ## Commands
31
+
32
+ | Task | Command |
33
+ |------|---------|
34
+ | One-time setup | `corepack enable && corepack prepare pnpm@10.33.0 --activate` |
35
+ | Validate the program | `make validate` |
36
+ | Compile to `app/generated/` | `make compile` |
37
+ | Install shell deps | `pnpm --dir app install` |
38
+ | Unit + generated test triads | `make test` |
39
+ | Lint custom skins (`app/skins/`) | `make lint-skins` (also runs in `make test`) |
40
+ | Dev server (http://localhost:3111) | `make dev` |
41
+ | Update toolchain + deps | `make update` |
42
+ | Production build | `make build` |
43
+ | Browser tests (a11y, perf) | `make test-e2e` (installs the browser on first run) |
44
+ | Screen previews (`screen-previews/*.png`, all screens, mobile + desktop) | `make preview` |
45
+
46
+ `make compile` requires the `ml1` compiler on PATH — install per README.
47
+ If `pnpm typecheck` complains about routes you deleted, remove the stale
48
+ Next cache: `rm -rf app/.next`.
49
+
50
+ ## Workflow for every change
51
+
52
+ 1. Edit `__APP_NAME__.ml`.
53
+ 2. `make compile` (validates first; fails on any violation).
54
+ 3. `make test`.
55
+ 4. Commit the `.ml` file **and** `app/generated/` together.
56
+
57
+ ## Shell rules (everything outside the .ml and app/generated/)
58
+
59
+ The shell is wiring and presentation only. Never put domain logic,
60
+ user-facing copy, or input validation in shell code — they belong in
61
+ `__APP_NAME__.ml`, where the validator and generated tests cover them.
62
+ Screens route through the single dynamic page
63
+ `app/app/screens/[screen]/page.tsx`; do not create per-screen folders.
64
+ Custom widget renderers ("skins") go in `app/skins/` — see the Skins
65
+ section of the language bundle; `make lint-skins` enforces the contract
66
+ (no hardcoded copy, no raw colors, no fetch/Date.now/adapter imports).
@@ -0,0 +1,3 @@
1
+ # CLAUDE.md
2
+
3
+ Read `AGENTS.md` — all agent instructions for this project live there.
package/template/Makefile CHANGED
@@ -1,6 +1,6 @@
1
1
  APP := __APP_NAME__
2
2
 
3
- .PHONY: validate compile build test test-e2e dev
3
+ .PHONY: validate compile build test test-e2e lint-skins preview dev update
4
4
 
5
5
  validate:
6
6
  ml1 validate $(APP).ml
@@ -13,9 +13,21 @@ build:
13
13
 
14
14
  test:
15
15
  pnpm --dir app run test
16
+ node scripts/lint-skins.mjs app/skins
17
+
18
+ lint-skins:
19
+ node scripts/lint-skins.mjs app/skins
16
20
 
17
21
  test-e2e:
22
+ pnpm --dir app exec playwright install chromium
18
23
  pnpm --dir app run test:e2e
19
24
 
25
+ preview:
26
+ pnpm --dir app exec playwright install chromium
27
+ pnpm --dir app run test:e2e e2e/99-screens.spec.ts
28
+
20
29
  dev:
21
30
  pnpm --dir app run dev
31
+
32
+ update:
33
+ ml1 update
@@ -5,11 +5,15 @@ A MinLang web app. The whole application lives in `__APP_NAME__.ml`;
5
5
  the thin Next.js shell consuming the published `@minlang/*` runtime.
6
6
 
7
7
  ```bash
8
+ corepack enable && corepack prepare pnpm@10.33.0 --activate
8
9
  make compile # __APP_NAME__.ml -> app/generated/ (needs ml1 on PATH)
9
10
  pnpm --dir app install
10
- pnpm --dir app dev
11
+ make dev # http://localhost:3111
11
12
  make test # generated test triads + integration
12
13
  ```
13
14
 
14
- Authoring guide:
15
- https://github.com/codeshift-ai-solutions/minlang-core/blob/main/docs/ai/language/WEB_APP_GUIDE.md
15
+ Install ml1:
16
+ `bash <(curl -fsSL https://raw.githubusercontent.com/codeshift-ai-solutions/minlang-releases/main/install/install.sh)`
17
+
18
+ Language rules + authoring guide (single file, for you and your coding agent):
19
+ https://github.com/codeshift-ai-solutions/minlang-releases/releases/latest/download/minlang-language-bundle.md
@@ -0,0 +1,17 @@
1
+ // Thin dynamic route over the generated screens — wrapper pages never go
2
+ // stale when the screen set changes. Unknown keys 404.
3
+ // Segment config must live in the page file itself for Next to see it.
4
+ export const dynamic = "force-dynamic";
5
+
6
+ import type { ReactElement } from "react";
7
+ import { notFound } from "next/navigation";
8
+ import { AppScreen } from "../../../generated/app/screen";
9
+ import { screenSchemas } from "../../../generated/ui/screens";
10
+
11
+ export default async function Page(props: {
12
+ params: Promise<{ screen: string }>;
13
+ }): Promise<ReactElement> {
14
+ const { screen } = await props.params;
15
+ if (!screenSchemas.some((s) => s.key === screen)) notFound();
16
+ return AppScreen({ screenKey: screen });
17
+ }
@@ -0,0 +1,32 @@
1
+ // Screen previews: a full-page screenshot of every manifest screen at
2
+ // mobile (390x844) and desktop (1280x800) viewports, written to
3
+ // screen-previews/ (gitignored) for the CI artifact and `make preview`.
4
+ // No assertions beyond successful page load. Runs last (99-) so data
5
+ // created by earlier specs makes the previews realistic.
6
+
7
+ import { mkdirSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import { expect, test } from "@playwright/test";
10
+ import manifest from "../generated/manifest.json";
11
+
12
+ const viewports = [
13
+ { name: "mobile", width: 390, height: 844 },
14
+ { name: "desktop", width: 1280, height: 800 },
15
+ ] as const;
16
+
17
+ const outDir = join(__dirname, "..", "screen-previews");
18
+
19
+ for (const screen of manifest.screens) {
20
+ for (const viewport of viewports) {
21
+ test(`preview: ${screen.key} (${viewport.name})`, async ({ page }) => {
22
+ await page.setViewportSize({ width: viewport.width, height: viewport.height });
23
+ await page.goto(screen.route, { waitUntil: "networkidle" });
24
+ await expect(page.getByRole("heading", { level: 1 }).first()).toBeVisible();
25
+ mkdirSync(outDir, { recursive: true });
26
+ await page.screenshot({
27
+ path: join(outDir, `${screen.key}-${viewport.name}.png`),
28
+ fullPage: true,
29
+ });
30
+ });
31
+ }
32
+ }
@@ -2,17 +2,17 @@
2
2
  "name": "__APP_NAME__-web",
3
3
  "private": true,
4
4
  "scripts": {
5
- "dev": "next dev",
5
+ "dev": "next dev --port 3111",
6
6
  "build": "next build",
7
7
  "start": "next start --port 3111",
8
- "typecheck": "tsc -p tsconfig.json",
8
+ "typecheck": "tsc -p tsconfig.typecheck.json",
9
9
  "test": "vitest run",
10
10
  "test:e2e": "next build && playwright test"
11
11
  },
12
12
  "dependencies": {
13
- "@minlang/design-system": "^0.1.0",
14
- "@minlang/runtime-web": "^0.1.0",
15
- "@minlang/tailwind-preset": "^0.1.0",
13
+ "@minlang/design-system": "^0.2.1",
14
+ "@minlang/runtime-web": "^0.2.1",
15
+ "@minlang/tailwind-preset": "^0.2.1",
16
16
  "next": "15.5.19",
17
17
  "react": "19.2.7",
18
18
  "react-dom": "19.2.7",
@@ -31,5 +31,11 @@
31
31
  "typescript": "5.6.3",
32
32
  "vitest": "3.2.6"
33
33
  },
34
- "packageManager": "pnpm@10.33.0"
34
+ "packageManager": "pnpm@10.33.0",
35
+ "pnpm": {
36
+ "onlyBuiltDependencies": [
37
+ "esbuild",
38
+ "sharp"
39
+ ]
40
+ }
35
41
  }
@@ -0,0 +1,15 @@
1
+ # Skins
2
+
3
+ Custom renderers ("skins") for widget keys, registered in `../seed.ts` via
4
+ `bindRegistryOverrides({ <widget>: Component })`. One skin per file
5
+ (`<widget-key>.tsx`), presentation only: props are exactly `WidgetProps<K>`,
6
+ copy comes only from the schema node, styling uses design-token classes
7
+ (never hex/rgb literals), and state changes go through the provided
8
+ server-action refs.
9
+
10
+ The full recipe (including adapting Figma Make exports) is the **Skins**
11
+ section of the language bundle:
12
+ https://github.com/codeshift-ai-solutions/minlang-releases/releases/latest/download/minlang-language-bundle.md
13
+
14
+ The contract is mechanically enforced — `make lint-skins`
15
+ (`node scripts/lint-skins.mjs app/skins`) runs as part of `make test` and CI.
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["node_modules", ".next"]
4
+ }
@@ -1,8 +1,11 @@
1
- # Verifies the committed generated output matches the .ml source (via the
2
- # minlang-compile action), tests, builds, and deploys prebuilt to Vercel.
3
- # Org-level secrets: VERCEL_TOKEN + VERCEL_ORG_ID (skips when absent).
4
- # The minlang actions require the minlang-core repo's Actions access setting
5
- # to allow this repository.
1
+ # Verifies the committed generated output matches the .ml source, tests,
2
+ # builds, and deploys prebuilt to Vercel. All logic lives in the public
3
+ # reusable workflow (minlang-releases) this file just calls it.
4
+ #
5
+ # Secrets (skips with a notice when absent):
6
+ # VERCEL_TOKEN + VERCEL_ORG_ID — org-level, set once for the whole org
7
+ # VERCEL_PROJECT_ID — per-repo; the first run links by name, prints the
8
+ # project id, and tells you to pin it as this secret
6
9
 
7
10
  name: Deploy (Vercel)
8
11
 
@@ -17,51 +20,7 @@ concurrency:
17
20
 
18
21
  jobs:
19
22
  deploy:
20
- runs-on: ubuntu-latest
21
- env:
22
- VERCEL_TOKEN: ${{ secrets.VERCEL_TOKEN }}
23
- VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
24
- steps:
25
- - uses: actions/checkout@v4
26
-
27
- - name: Generated output is current
28
- uses: codeshift-ai-solutions/minlang-core/.github/actions/minlang-compile@main
29
- with:
30
- file: __APP_NAME__.ml
31
- target: web
32
- out: app
33
- check: "true"
34
-
35
- - name: Setup Node + pnpm
36
- uses: actions/setup-node@v4
37
- with:
38
- node-version: 22
39
- - run: corepack enable
40
-
41
- - name: Install, typecheck, test
42
- working-directory: app
43
- run: |
44
- pnpm install --frozen-lockfile
45
- pnpm run typecheck
46
- pnpm run test
47
-
48
- - name: Gate on Vercel secrets
49
- id: gate
50
- run: |
51
- if [ -z "${VERCEL_TOKEN}" ] || [ -z "${VERCEL_ORG_ID}" ]; then
52
- echo "notice: VERCEL_TOKEN / VERCEL_ORG_ID not set — skipping deploy"
53
- echo "enabled=false" >> "$GITHUB_OUTPUT"
54
- else
55
- echo "enabled=true" >> "$GITHUB_OUTPUT"
56
- fi
57
-
58
- - name: Link, build, deploy
59
- if: steps.gate.outputs.enabled == 'true'
60
- working-directory: app
61
- run: |
62
- pnpm dlx vercel@latest link --yes \
63
- --project "${{ github.event.repository.name }}" --token "${VERCEL_TOKEN}"
64
- pnpm dlx vercel@latest pull --yes --environment=production --token "${VERCEL_TOKEN}"
65
- pnpm dlx vercel@latest build --prod --token "${VERCEL_TOKEN}"
66
- url=$(pnpm dlx vercel@latest deploy --prebuilt --prod --yes --token "${VERCEL_TOKEN}")
67
- echo "deployed: $url" | tee -a "$GITHUB_STEP_SUMMARY"
23
+ uses: codeshift-ai-solutions/minlang-releases/.github/workflows/deploy-vercel.yml@main
24
+ with:
25
+ file: __APP_NAME__.ml
26
+ secrets: inherit
@@ -3,6 +3,7 @@ node_modules/
3
3
  .vercel/
4
4
  playwright-report/
5
5
  test-results/
6
+ screen-previews/
6
7
  coverage/
7
8
  *.tsbuildinfo
8
9
  .env
@@ -0,0 +1,197 @@
1
+ #!/usr/bin/env node
2
+ // Mechanical SHELL_CONTRACT lint for registry skins (spec/target-web/SKIN_GUIDE.md).
3
+ //
4
+ // Usage: node scripts/lint-skins.mjs [dir...] (default: app/skins, if it exists)
5
+ //
6
+ // Scans .ts/.tsx files line by line and exits 1 with file:line findings on:
7
+ // jsx-text hardcoded copy as JSX text (.tsx only — heuristic below)
8
+ // color-literal #hex (3/4/6/8) · rgb(/rgba(/hsl(/hsla( · Tailwind -[# classes
9
+ // forbidden-api fetch( · Date.now · Math.random · new Date( · toLocale*
10
+ // adapter-import imports of @minlang/runtime-web/server or any */adapters/* path
11
+ //
12
+ // jsx-text heuristic (documented contract): string literals are blanked, the
13
+ // line is cut at any // comment, =>/<=/>= and space-surrounded </> comparison
14
+ // operators are blanked, {…} JSX expressions are removed; the line is flagged when (a) letters remain between a `>` and a `<`
15
+ // on the same line, or (b) the line is bare prose (letters/spaces/simple
16
+ // punctuation, not a JS keyword) and the previous non-blank line ends with `>`
17
+ // but not `=>`. Block comments are not masked. The shipped design system must
18
+ // lint clean (regression-tested in scripts/tests/lint-skins.test.mjs).
19
+ //
20
+ // Opt-out, per offending line: trailing `// skin-lint-allow: <rule>[, <rule>...]`.
21
+ // Exit codes: 0 clean · 1 findings · 2 usage error (explicit dir missing).
22
+
23
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
24
+ import { join } from "node:path";
25
+
26
+ const HEX_COLOR = /#(?:[0-9a-fA-F]{8}|[0-9a-fA-F]{6}|[0-9a-fA-F]{4}|[0-9a-fA-F]{3})\b/;
27
+ const COLOR_FN = /\b(?:rgb|rgba|hsl|hsla)\s*\(/;
28
+ const ARBITRARY_COLOR = /-\[#/;
29
+ const FORBIDDEN_APIS = [
30
+ [/\bfetch\s*\(/, "fetch("],
31
+ [/\bDate\.now\b/, "Date.now"],
32
+ [/\bMath\.random\b/, "Math.random"],
33
+ [/\bnew\s+Date\s*\(/, "new Date("],
34
+ [/toLocale/, "toLocale* (locale-ambient formatting)"],
35
+ ];
36
+ const SERVER_IMPORT = /@minlang\/runtime-web\/server/;
37
+ const ADAPTER_PATH = /["'][^"']*\/adapters\/[^"']*["']/;
38
+ const JS_KEYWORDS = new Set([
39
+ "return", "break", "continue", "else", "try", "finally", "default", "do",
40
+ "case", "new", "typeof", "void", "delete", "await", "yield", "async",
41
+ "export", "import", "const", "let", "var", "function", "class", "if",
42
+ "for", "while", "switch", "throw", "true", "false", "null", "undefined",
43
+ ]);
44
+ const SKIP_DIRS = new Set(["node_modules", "dist", "generated", "coverage"]);
45
+
46
+ /** Blank string-literal contents, preserving length and the quote chars. */
47
+ const blankStrings = (line) =>
48
+ line.replace(/"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|`(?:[^`\\]|\\.)*`/g, (m) =>
49
+ m[0] + " ".repeat(m.length - 2) + m[0],
50
+ );
51
+
52
+ /** Remove {…} JSX expressions and blank arrow/comparison operators. */
53
+ const dropExpressions = (text) => {
54
+ let t = text.replace(/=>|<=|>=/g, " ").replace(/ [<>] /g, " ");
55
+ for (let prev = ""; prev !== t; ) {
56
+ prev = t;
57
+ t = t.replace(/\{[^{}]*\}/g, "");
58
+ }
59
+ return t;
60
+ };
61
+
62
+ const BARE_PROSE = /^[A-Za-z][A-Za-z0-9 ,.'!?-]*$/;
63
+
64
+ /** Lint one file; returns findings [{line, rule, message}]. */
65
+ const lintFile = (path) => {
66
+ const findings = [];
67
+ const isTsx = path.endsWith(".tsx");
68
+ let prevCode = "";
69
+ const lines = readFileSync(path, "utf8").split("\n");
70
+ lines.forEach((raw, index) => {
71
+ const blanked = blankStrings(raw);
72
+ const commentIdx = blanked.indexOf("//");
73
+ const allowed = new Set();
74
+ let code = raw;
75
+ let codeBlanked = blanked;
76
+ if (commentIdx !== -1) {
77
+ const match = raw.slice(commentIdx).match(/skin-lint-allow:\s*([a-z, -]+)/);
78
+ for (const rule of match === null ? [] : match[1].split(",")) {
79
+ allowed.add(rule.trim());
80
+ }
81
+ code = raw.slice(0, commentIdx);
82
+ codeBlanked = blanked.slice(0, commentIdx);
83
+ }
84
+ const report = (rule, message) => {
85
+ if (!allowed.has(rule)) {
86
+ findings.push({ line: index + 1, rule, message });
87
+ }
88
+ };
89
+ checkColors(code, report);
90
+ checkForbiddenApis(codeBlanked, report);
91
+ checkAdapterImports(code, report);
92
+ if (isTsx) {
93
+ checkJsxText(codeBlanked, prevCode, report);
94
+ }
95
+ if (code.trim() !== "") {
96
+ prevCode = code;
97
+ }
98
+ });
99
+ return findings;
100
+ };
101
+
102
+ const checkColors = (code, report) => {
103
+ if (HEX_COLOR.test(code)) {
104
+ report("color-literal", "hex color literal — style via design tokens / token classes");
105
+ }
106
+ if (COLOR_FN.test(code)) {
107
+ report("color-literal", "rgb()/hsl() color literal — style via design tokens");
108
+ }
109
+ if (ARBITRARY_COLOR.test(code)) {
110
+ report("color-literal", "Tailwind arbitrary color class — use token classes");
111
+ }
112
+ };
113
+
114
+ const checkForbiddenApis = (codeBlanked, report) => {
115
+ for (const [pattern, name] of FORBIDDEN_APIS) {
116
+ if (pattern.test(codeBlanked)) {
117
+ report("forbidden-api", `${name} is forbidden in skins (SHELL_CONTRACT nondeterminism/IO)`);
118
+ }
119
+ }
120
+ };
121
+
122
+ const checkAdapterImports = (code, report) => {
123
+ if (SERVER_IMPORT.test(code)) {
124
+ report("adapter-import", "@minlang/runtime-web/server import — skins never touch the adapter");
125
+ }
126
+ if (ADAPTER_PATH.test(code)) {
127
+ report("adapter-import", "adapter/store import — skins read only the schema node and deps");
128
+ }
129
+ };
130
+
131
+ const checkJsxText = (codeBlanked, prevCode, report) => {
132
+ const masked = dropExpressions(codeBlanked);
133
+ const between = masked.match(/>([^<>]*[A-Za-z][^<>]*)</);
134
+ if (between !== null) {
135
+ report("jsx-text", `hardcoded JSX text "${between[1].trim()}" — copy must come from the schema node`);
136
+ return;
137
+ }
138
+ const bare = masked.trim();
139
+ const prev = prevCode.trim();
140
+ if (
141
+ BARE_PROSE.test(bare) &&
142
+ !JS_KEYWORDS.has(bare) &&
143
+ prev.endsWith(">") &&
144
+ !prev.endsWith("=>")
145
+ ) {
146
+ report("jsx-text", `hardcoded JSX text "${bare}" — copy must come from the schema node`);
147
+ }
148
+ };
149
+
150
+ const collectFiles = (dir, out) => {
151
+ for (const entry of readdirSync(dir).sort()) {
152
+ if (entry.startsWith(".") || SKIP_DIRS.has(entry)) {
153
+ continue;
154
+ }
155
+ const path = join(dir, entry);
156
+ if (statSync(path).isDirectory()) {
157
+ collectFiles(path, out);
158
+ } else if (/\.tsx?$/.test(entry)) {
159
+ out.push(path);
160
+ }
161
+ }
162
+ return out;
163
+ };
164
+
165
+ const main = () => {
166
+ const args = process.argv.slice(2);
167
+ let dirs = args;
168
+ if (args.length === 0) {
169
+ if (!existsSync("app/skins")) {
170
+ console.log("lint-skins: no app/skins directory — nothing to lint");
171
+ return 0;
172
+ }
173
+ dirs = ["app/skins"];
174
+ }
175
+ for (const dir of dirs) {
176
+ if (!existsSync(dir)) {
177
+ console.error(`lint-skins: no such directory: ${dir}`);
178
+ return 2;
179
+ }
180
+ }
181
+ const files = dirs.flatMap((dir) => collectFiles(dir, []));
182
+ let total = 0;
183
+ for (const file of files) {
184
+ for (const finding of lintFile(file)) {
185
+ total += 1;
186
+ console.error(`${file}:${finding.line}: [${finding.rule}] ${finding.message}`);
187
+ }
188
+ }
189
+ if (total > 0) {
190
+ console.error(`lint-skins: ${total} finding(s) in ${files.length} file(s)`);
191
+ return 1;
192
+ }
193
+ console.log(`lint-skins: ${files.length} file(s) clean`);
194
+ return 0;
195
+ };
196
+
197
+ process.exit(main());
@@ -1,5 +0,0 @@
1
- // Thin re-export of the generated route (generated/ is the app).
2
- // Segment config must live in the page file itself for Next to see it.
3
- export const dynamic = "force-dynamic";
4
-
5
- export { default } from "../../../generated/app/screens/board/page";
@@ -1,5 +0,0 @@
1
- // Thin re-export of the generated route (generated/ is the app).
2
- // Segment config must live in the page file itself for Next to see it.
3
- export const dynamic = "force-dynamic";
4
-
5
- export { default } from "../../../generated/app/screens/projects/page";