@drhomet/mithaq 0.0.0-stage → 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/LICENSE +22 -0
- package/LICENSES/openstatus-time-picker.txt +21 -0
- package/LICENSES/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/README.md +160 -2
- package/dist/asas/actor.d.ts +96 -0
- package/dist/asas/actor.js +114 -0
- package/dist/asas/build.d.ts +239 -0
- package/dist/asas/build.js +834 -0
- package/dist/asas/cli/mithaq.d.ts +2 -0
- package/dist/asas/cli/mithaq.js +177 -0
- package/dist/asas/cli/upgrade.d.ts +33 -0
- package/dist/asas/cli/upgrade.js +256 -0
- package/dist/asas/http/fetch.d.ts +41 -0
- package/dist/asas/http/fetch.js +178 -0
- package/dist/asas/http/orpc.d.ts +45 -0
- package/dist/asas/http/orpc.js +153 -0
- package/dist/asas/runner/bulk.d.ts +20 -0
- package/dist/asas/runner/bulk.js +196 -0
- package/dist/asas/runner/errors.d.ts +63 -0
- package/dist/asas/runner/errors.js +75 -0
- package/dist/asas/runner/read.d.ts +72 -0
- package/dist/asas/runner/read.js +601 -0
- package/dist/asas/runner/runtime.d.ts +21 -0
- package/dist/asas/runner/runtime.js +8 -0
- package/dist/asas/runner/steps.d.ts +418 -0
- package/dist/asas/runner/steps.js +277 -0
- package/dist/asas/runner/store.d.ts +4 -0
- package/dist/asas/runner/store.js +21 -0
- package/dist/asas/runner/tenant.d.ts +54 -0
- package/dist/asas/runner/tenant.js +275 -0
- package/dist/asas/runner/write.d.ts +606 -0
- package/dist/asas/runner/write.js +1019 -0
- package/dist/auth/better-auth/access.d.ts +29 -0
- package/dist/auth/better-auth/access.js +103 -0
- package/dist/auth/better-auth/generate.d.ts +31 -0
- package/dist/auth/better-auth/generate.js +328 -0
- package/dist/auth/better-auth/index.d.ts +94 -0
- package/dist/auth/better-auth/index.js +81 -0
- package/dist/auth/better-auth/tenant.d.ts +26 -0
- package/dist/auth/better-auth/tenant.js +34 -0
- package/dist/auth/local/index.d.ts +285 -0
- package/dist/auth/local/index.js +738 -0
- package/dist/base/decimal.d.ts +9 -0
- package/dist/base/decimal.js +16 -0
- package/dist/base/fields.d.ts +124 -0
- package/dist/base/fields.js +52 -0
- package/dist/base/index.d.ts +47 -0
- package/dist/base/index.js +5 -0
- package/dist/base/json-schema.d.ts +7 -0
- package/dist/base/json-schema.js +35 -0
- package/dist/base/kinds.d.ts +75 -0
- package/dist/base/kinds.js +123 -0
- package/dist/base/rules.d.ts +73 -0
- package/dist/base/rules.js +81 -0
- package/dist/drizzle/index.d.ts +308 -0
- package/dist/drizzle/index.js +951 -0
- package/dist/drizzle/kinds.d.ts +28 -0
- package/dist/drizzle/kinds.js +43 -0
- package/dist/drizzle/migrate.d.ts +159 -0
- package/dist/drizzle/migrate.js +1328 -0
- package/dist/drizzle/proxy/index.d.ts +1 -0
- package/dist/drizzle/proxy/index.js +1 -0
- package/dist/drizzle/proxy/node-postgres.d.ts +1 -0
- package/dist/drizzle/proxy/node-postgres.js +1 -0
- package/dist/drizzle/proxy/pg-core.d.ts +1 -0
- package/dist/drizzle/proxy/pg-core.js +1 -0
- package/dist/drizzle/rules.d.ts +336 -0
- package/dist/drizzle/rules.js +44 -0
- package/dist/index.d.ts +35 -0
- package/dist/index.js +26 -0
- package/dist/mithaq/define.d.ts +335 -0
- package/dist/mithaq/define.js +57 -0
- package/dist/mithaq/generate-messages.d.ts +38 -0
- package/dist/mithaq/generate-messages.js +235 -0
- package/dist/mithaq/language.d.ts +374 -0
- package/dist/mithaq/language.js +76 -0
- package/dist/mithaq/lock.d.ts +71 -0
- package/dist/mithaq/lock.js +329 -0
- package/dist/mithaq/message-file.d.ts +13 -0
- package/dist/mithaq/message-file.js +29 -0
- package/dist/mithaq/messages.d.ts +55 -0
- package/dist/mithaq/messages.js +111 -0
- package/dist/mithaq/refusal.d.ts +6 -0
- package/dist/mithaq/refusal.js +6 -0
- package/dist/mithaq/resolve.d.ts +10 -0
- package/dist/mithaq/resolve.js +1014 -0
- package/dist/mithaq/rows.d.ts +87 -0
- package/dist/mithaq/rows.js +174 -0
- package/dist/mithaq/rukn.d.ts +72 -0
- package/dist/mithaq/rukn.js +15 -0
- package/dist/mithaq/standard-schema.d.ts +38 -0
- package/dist/mithaq/standard-schema.js +2 -0
- package/dist/mithaq/types.d.ts +386 -0
- package/dist/mithaq/types.js +15 -0
- package/dist/mithaq/user.d.ts +19 -0
- package/dist/mithaq/user.js +16 -0
- package/dist/mithaq/where.d.ts +32 -0
- package/dist/mithaq/where.js +87 -0
- package/dist/rbac/index.d.ts +105 -0
- package/dist/rbac/index.js +182 -0
- package/dist/rbac/steps.d.ts +22 -0
- package/dist/rbac/steps.js +161 -0
- package/dist/rich-text/index.d.ts +28 -0
- package/dist/rich-text/index.js +59 -0
- package/dist/rich-text/rich-text.d.ts +106 -0
- package/dist/rich-text/rich-text.js +249 -0
- package/dist/rich-text/value.d.ts +55 -0
- package/dist/rich-text/value.js +126 -0
- package/dist/screen/client.d.ts +156 -0
- package/dist/screen/client.js +71 -0
- package/dist/screen/generate.d.ts +14 -0
- package/dist/screen/generate.js +268 -0
- package/dist/screen/index.d.ts +496 -0
- package/dist/screen/index.js +663 -0
- package/dist/screen/react.d.ts +97 -0
- package/dist/screen/react.js +58 -0
- package/dist/screen/studio.d.ts +24 -0
- package/dist/screen/studio.js +68 -0
- package/dist/service/index.d.ts +63 -0
- package/dist/service/index.js +15 -0
- package/dist/storage/allowed-type.d.ts +5 -0
- package/dist/storage/allowed-type.js +7 -0
- package/dist/storage/index.d.ts +42 -0
- package/dist/storage/index.js +43 -0
- package/dist/storage/storage.d.ts +386 -0
- package/dist/storage/storage.js +412 -0
- package/dist/targets.d.ts +14 -0
- package/dist/targets.js +16 -0
- package/dist/versions/index.d.ts +37 -0
- package/dist/versions/index.js +8 -0
- package/dist/versions/versions.d.ts +104 -0
- package/dist/versions/versions.js +160 -0
- package/dist/vite.d.ts +7 -0
- package/dist/vite.js +81 -0
- package/dist/zod/index.d.ts +193 -0
- package/dist/zod/index.js +122 -0
- package/dist/zod/kinds.d.ts +36 -0
- package/dist/zod/kinds.js +58 -0
- package/dist/zod/rules.d.ts +117 -0
- package/dist/zod/rules.js +67 -0
- package/docs/packages/mithaq/README.md +962 -0
- package/docs/packages/mithaq/src/asas/cli/README.md +252 -0
- package/docs/packages/mithaq/src/asas/runner/README.md +472 -0
- package/docs/packages/mithaq/src/drizzle/README.md +39 -0
- package/docs/packages/mithaq-admin/README.md +312 -0
- package/docs/templates/asas/README.md +1056 -0
- package/package.json +92 -4
- package/skills/add-field/SKILL.md +27 -0
- package/skills/add-kayan/SKILL.md +26 -0
- package/skills/eject-admin-piece/SKILL.md +28 -0
- package/skills/upgrade-mithaq/SKILL.md +35 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type ScreenClient } from './index.ts';
|
|
2
|
+
/**
|
|
3
|
+
* Writes each screen's `screen.json`, `types.ts`, `files.ts` and route files under `into`, and answers the
|
|
4
|
+
* stale files. With `check` it writes nothing. `into` must sit under a TanStack Router `routes` folder.
|
|
5
|
+
*/
|
|
6
|
+
export declare const generateScreens: ({ root, screens, into, locales, check, }: {
|
|
7
|
+
root: URL;
|
|
8
|
+
screens: Readonly<Record<string, ScreenClient>>;
|
|
9
|
+
into: URL;
|
|
10
|
+
locales?: readonly string[];
|
|
11
|
+
check?: boolean;
|
|
12
|
+
}) => Promise<string[]>;
|
|
13
|
+
/** The command, given the project's own root, answering the code it exits with. */
|
|
14
|
+
export declare const run: (project: URL, check: boolean) => Promise<number>;
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync, realpathSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { dirname, isAbsolute, join, relative, sep } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
|
+
import { isIdentifierName } from '@babel/helper-validator-identifier';
|
|
8
|
+
import { packageOf } from './index.js';
|
|
9
|
+
import { referencesIn } from './client.js';
|
|
10
|
+
import { Refusal } from '../mithaq/refusal.js';
|
|
11
|
+
const typesHeader = '// Written by `mithaq generate screens`. Do not edit.';
|
|
12
|
+
const exec = promisify(execFile);
|
|
13
|
+
// Not shared with `generate auth`: that module imports `better-auth/db`, which
|
|
14
|
+
// `generate screens` would then load for one line.
|
|
15
|
+
const sourceString = (value) => `'${JSON.stringify(value).slice(1, -1).replaceAll("'", "\\'")}'`;
|
|
16
|
+
const camel = (name) => name.replace(/[-_](.)/g, (_, letter) => letter.toUpperCase());
|
|
17
|
+
const pascal = (name) => {
|
|
18
|
+
const cased = camel(name);
|
|
19
|
+
return cased.charAt(0).toUpperCase() + cased.slice(1);
|
|
20
|
+
};
|
|
21
|
+
const sourceKey = (name) => (isIdentifierName(name) ? name : sourceString(name));
|
|
22
|
+
// One type per kayan of the screen from its fields, which `useValues<CoursesValues>()` is typed from.
|
|
23
|
+
const typesOf = (client, locales) => {
|
|
24
|
+
const kayan = Object.values(client.kayan);
|
|
25
|
+
const nameOf = (name) => `${/^[0-9]/.test(name) ? '_' : ''}${pascal(name)}Values`;
|
|
26
|
+
const names = kayan.map((one) => nameOf(one.name));
|
|
27
|
+
if (new Set(names).size !== names.length)
|
|
28
|
+
throw new Refusal('Two kayan of one screen generate the same values type name. Rename one kayan.');
|
|
29
|
+
const idOf = (name) => {
|
|
30
|
+
const kind = client.kayan[name]?.fields.find((field) => field.name === 'id')?.kind;
|
|
31
|
+
return kind === 'number' ? 'number' : kind === 'text' || kind === 'uuid' ? 'string' : 'unknown';
|
|
32
|
+
};
|
|
33
|
+
const fieldType = (field) => {
|
|
34
|
+
let value;
|
|
35
|
+
if (field.kind === 'text' && field.options?.length)
|
|
36
|
+
value = field.options.map((option) => sourceString(option.value)).join(' | ');
|
|
37
|
+
else if (field.rules.localized === true)
|
|
38
|
+
value = `Record<${locales.map(sourceString).join(' | ') || 'string'}, string>`;
|
|
39
|
+
else if (field.kind === 'relationship' || field.kind === 'upload')
|
|
40
|
+
value = `${idOf(field.relationTo ?? '')}${field.hasMany ? '[]' : ''}`;
|
|
41
|
+
// A join to a kayan this screen does not draw has no type here to name.
|
|
42
|
+
else if (field.kind === 'join')
|
|
43
|
+
value = field.relationTo && client.kayan[field.relationTo] ? `Values[${sourceString(field.relationTo)}][]` : 'unknown[]';
|
|
44
|
+
else
|
|
45
|
+
value = { text: 'string', email: 'string', uuid: 'string', date: 'string', number: 'number', integer: 'number', decimal: 'string', checkbox: 'boolean', datetime: 'Date', textArray: 'string[]', richText: '{ root: Record<string, unknown> }' }[field.kind] ?? 'unknown';
|
|
46
|
+
// A stored row always holds an id, so it is never `| null`.
|
|
47
|
+
return field.name !== 'id' && field.kind !== 'join' && field.rules.required !== true && value !== 'unknown' ? `${value} | null` : value;
|
|
48
|
+
};
|
|
49
|
+
const values = kayan.map((one, at) => `export type ${names[at]} = {\n${one.fields.map((field) => ` ${sourceKey(field.name)}: ${fieldType(field)}`).join('\n')}\n}`);
|
|
50
|
+
const pages = kayan.map((one) => ` ${sourceKey(one.name)}: ${Object.keys(one.pages).map(sourceString).join(' | ') || 'never'}`);
|
|
51
|
+
return `${typesHeader}
|
|
52
|
+
import type { ScreenClient as Client } from 'mithaq/admin'
|
|
53
|
+
export type { PageClient } from 'mithaq/admin'
|
|
54
|
+
${values.join('\n')}
|
|
55
|
+
export type Values = {
|
|
56
|
+
${kayan.map((one, at) => ` ${sourceKey(one.name)}: ${names[at]}`).join('\n')}
|
|
57
|
+
}
|
|
58
|
+
export type Pages = {
|
|
59
|
+
${pages.join('\n')}
|
|
60
|
+
}
|
|
61
|
+
export type ScreenClient = Client<Pages>
|
|
62
|
+
`;
|
|
63
|
+
};
|
|
64
|
+
// One literal, lazy `import.meta.glob` per package over exactly the files the
|
|
65
|
+
// references name: Vite resolves a glob at build and takes no variable.
|
|
66
|
+
const globOf = ({ screen, client, root, app, appName, from }) => {
|
|
67
|
+
// Each base is written from the screen's own folder into the project, the
|
|
68
|
+
// folder holding both the language and the app: the workspace root of a
|
|
69
|
+
// monorepo, the package itself otherwise. `files.ts` is committed, so a base
|
|
70
|
+
// never names a folder outside it, which differs from machine to machine.
|
|
71
|
+
const realApp = realpathSync(fileURLToPath(app));
|
|
72
|
+
const here = join(realApp, relative(fileURLToPath(app), fileURLToPath(from)));
|
|
73
|
+
const ours = realpathSync(fileURLToPath(root)).split(sep);
|
|
74
|
+
const theirs = realApp.split(sep);
|
|
75
|
+
let shared = 0;
|
|
76
|
+
while (shared < ours.length && ours[shared] === theirs[shared])
|
|
77
|
+
shared++;
|
|
78
|
+
const project = ours.slice(0, shared).join(sep) || sep;
|
|
79
|
+
const inProject = (folder) => !isAbsolute(relative(project, folder)) && relative(project, folder).split(sep)[0] !== '..';
|
|
80
|
+
const baseOf = (folder) => {
|
|
81
|
+
if (!inProject(folder))
|
|
82
|
+
throw new Refusal(`The ${screen} screen's files are read from ${folder}, outside the project at ${project}.`);
|
|
83
|
+
return `${relative(here, folder).replaceAll('\\', '/')}/`;
|
|
84
|
+
};
|
|
85
|
+
const linkFrom = ({ start, name }) => {
|
|
86
|
+
for (let folder = start;; folder = dirname(folder)) {
|
|
87
|
+
const link = join(folder, 'node_modules', name);
|
|
88
|
+
if (existsSync(link))
|
|
89
|
+
return link;
|
|
90
|
+
if (folder === project || dirname(folder) === folder)
|
|
91
|
+
return undefined;
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
// A workspace package is read where it is, which npm, pnpm, yarn and Bun
|
|
95
|
+
// agree on. Any other is read through its `node_modules` link, because the
|
|
96
|
+
// folder behind it names a version and a hash, or lies outside the project.
|
|
97
|
+
// An app that depends on the asas alone links no feature package under an
|
|
98
|
+
// isolated install (pnpm, Bun), so the asas, which depends on each, is asked next.
|
|
99
|
+
const installed = (name) => {
|
|
100
|
+
const link = linkFrom({ start: realApp, name }) ?? linkFrom({ start: ours.join(sep), name });
|
|
101
|
+
if (!link)
|
|
102
|
+
throw new Refusal(`The ${screen} screen names files of ${name}, which no node_modules in the project holds. Install it in the app.`);
|
|
103
|
+
const real = realpathSync(link);
|
|
104
|
+
return baseOf(inProject(real) && !relative(project, real).split(sep).includes('node_modules') ? real : link);
|
|
105
|
+
};
|
|
106
|
+
const linked = app.href === root.href ? '/' : baseOf(ours.join(sep));
|
|
107
|
+
const byPackage = new Map();
|
|
108
|
+
for (const key of new Set(referencesIn(client).map((reference) => reference.slice(0, reference.indexOf('#'))))) {
|
|
109
|
+
const name = key.startsWith('./') ? '' : key.slice(0, key.indexOf(':'));
|
|
110
|
+
byPackage.set(name, [...(byPackage.get(name) ?? []), name ? key.slice(name.length + 1) : key]);
|
|
111
|
+
}
|
|
112
|
+
const glob = (files, base) => `import.meta.glob([${files.sort().map(sourceString).join(', ')}], { base: ${sourceString(base)}${base === '/' ? '' : ', exhaustive: true'} })`;
|
|
113
|
+
const bare = byPackage.get('') ?? [];
|
|
114
|
+
// `screen.json` prints a path decoded, which `new URL` would misread at a `%` or a `?`.
|
|
115
|
+
const base = bare.length && bare.every((file) => existsSync(join(fileURLToPath(app), file))) ? '/' : linked;
|
|
116
|
+
const at = base === '/' ? app : root;
|
|
117
|
+
const missing = bare.filter((file) => !existsSync(join(fileURLToPath(at), file)));
|
|
118
|
+
if (missing.length) {
|
|
119
|
+
throw new Refusal(`The ${screen} screen's root page is drawn from a package that is neither the app serving it nor the project: ${missing.join(', ')} ${missing.length === 1 ? 'is' : 'are'} in neither. Draw a screen's root page from the app or the project.`);
|
|
120
|
+
}
|
|
121
|
+
const own = glob(bare, base);
|
|
122
|
+
const named = [...byPackage.keys()].filter(Boolean).sort();
|
|
123
|
+
if (!named.length) {
|
|
124
|
+
return `${typesHeader}
|
|
125
|
+
/// <reference types="vite/client" />
|
|
126
|
+
// Every file the \`${screen}\` screen's references name, each loaded when a page draws it.
|
|
127
|
+
export const files = ${own}
|
|
128
|
+
`;
|
|
129
|
+
}
|
|
130
|
+
return `${typesHeader}
|
|
131
|
+
/// <reference types="vite/client" />
|
|
132
|
+
// Every file the \`${screen}\` screen's references name, each loaded when a page draws it, keyed as the reference prints it.
|
|
133
|
+
const named = (name: string, files: Record<string, () => Promise<unknown>>) => Object.fromEntries(Object.entries(files).map(([file, load]) => [\`\${name}:\${file}\`, load]))
|
|
134
|
+
export const files = {
|
|
135
|
+
${bare.length ? ` ...${own},\n` : ''}${named.map((name) => ` ...named(${sourceString(name)}, ${glob(byPackage.get(name), name === appName ? '/' : installed(name))}),\n`).join('')}}
|
|
136
|
+
`;
|
|
137
|
+
};
|
|
138
|
+
const routesOf = (screen, client, prefix) => {
|
|
139
|
+
const segments = client.path.split('/').filter(Boolean);
|
|
140
|
+
const base = segments.join('.');
|
|
141
|
+
const id = `${prefix}${segments.join('/')}`;
|
|
142
|
+
const name = sourceString(screen);
|
|
143
|
+
const imports = (names, host) => `${typesHeader}
|
|
144
|
+
import { ${names} } from '@tanstack/react-router'
|
|
145
|
+
import { ${host} } from '@/screens.tsx'
|
|
146
|
+
import { files } from './${screen}/files.ts'
|
|
147
|
+
`;
|
|
148
|
+
return [
|
|
149
|
+
[`${base}.tsx`, `${imports('Outlet, createFileRoute', 'ScreenFrame, frameLoader')}
|
|
150
|
+
export const Route = createFileRoute('${id}')({
|
|
151
|
+
loader: ({ location }) => frameLoader({ screen: ${name}, files, href: location.href }),
|
|
152
|
+
component: () => (
|
|
153
|
+
<ScreenFrame files={files} data={Route.useLoaderData()}>
|
|
154
|
+
<Outlet />
|
|
155
|
+
</ScreenFrame>
|
|
156
|
+
),
|
|
157
|
+
})
|
|
158
|
+
`],
|
|
159
|
+
[`${base}.index.tsx`, `${imports('createFileRoute', 'ScreenPageView, askedFor, pageHead, pageLoader')}
|
|
160
|
+
export const Route = createFileRoute('${id}/')({
|
|
161
|
+
loader: ({ location }) => pageLoader({ screen: ${name}, files, path: '', query: {}, href: location.href }),
|
|
162
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
163
|
+
component: () => <ScreenPageView files={files} data={Route.useLoaderData()} query={askedFor({})} onQuery={() => {}} />,
|
|
164
|
+
})
|
|
165
|
+
`],
|
|
166
|
+
[`${base}.$.tsx`, `${imports('createFileRoute', 'ScreenNotFound, ScreenPageView, askedFor, pageHead, pageLoader')}
|
|
167
|
+
export const Route = createFileRoute('${id}/$')({
|
|
168
|
+
validateSearch: askedFor,
|
|
169
|
+
loader: ({ location, params }) => pageLoader({ screen: ${name}, files, path: params._splat ?? '', query: location.search, href: location.href }),
|
|
170
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
171
|
+
notFoundComponent: () => <ScreenNotFound files={files} screen={${name}} />,
|
|
172
|
+
component: Page,
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
function Page() {
|
|
176
|
+
const navigate = Route.useNavigate()
|
|
177
|
+
return <ScreenPageView files={files} data={Route.useLoaderData()} query={Route.useSearch()} onQuery={(search) => void navigate({ search })} />
|
|
178
|
+
}
|
|
179
|
+
`],
|
|
180
|
+
];
|
|
181
|
+
};
|
|
182
|
+
/**
|
|
183
|
+
* Writes each screen's `screen.json`, `types.ts`, `files.ts` and route files under `into`, and answers the
|
|
184
|
+
* stale files. With `check` it writes nothing. `into` must sit under a TanStack Router `routes` folder.
|
|
185
|
+
*/
|
|
186
|
+
export const generateScreens = async ({ root, screens, into, locales = [], check = false, }) => {
|
|
187
|
+
const stale = [];
|
|
188
|
+
const named = (at) => relative(fileURLToPath(root), fileURLToPath(at)).replaceAll('\\', '/');
|
|
189
|
+
const routesAt = into.pathname.lastIndexOf('/routes/');
|
|
190
|
+
if (routesAt < 0 && Object.keys(screens).length) {
|
|
191
|
+
throw new Refusal(`${named(into)}/ is under no \`routes\` folder, and \`mithaq generate screens\` writes each screen's route files there. Name a folder under src/routes/ in generate.screens.`);
|
|
192
|
+
}
|
|
193
|
+
const prefix = decodeURIComponent(into.pathname.slice(routesAt + '/routes'.length));
|
|
194
|
+
// The app is the package holding `into`, and Vite's root is its folder.
|
|
195
|
+
const app = packageOf(into) ?? root;
|
|
196
|
+
const appName = JSON.parse(await readFile(new URL('package.json', app), 'utf8').catch(() => '{}')).name;
|
|
197
|
+
const wanted = Object.entries(screens).flatMap(([key, client]) => {
|
|
198
|
+
const from = new URL(`${key}/`, into);
|
|
199
|
+
return [
|
|
200
|
+
[new URL('screen.json', from), `${JSON.stringify(client, null, 2)}\n`],
|
|
201
|
+
[new URL('types.ts', from), typesOf(client, locales)],
|
|
202
|
+
[new URL('files.ts', from), globOf({ screen: key, client, root, app, appName, from })],
|
|
203
|
+
...routesOf(key, client, prefix).map(([file, source]) => [new URL(file, into), source]),
|
|
204
|
+
];
|
|
205
|
+
});
|
|
206
|
+
const before = await Promise.all(wanted.map(([at]) => readFile(at, 'utf8').catch(() => undefined)));
|
|
207
|
+
// A file of the project's own is never overwritten, and nothing is written until every file has been asked.
|
|
208
|
+
wanted.forEach(([at], index) => {
|
|
209
|
+
const held = before[index];
|
|
210
|
+
if (held !== undefined && !at.pathname.endsWith('.json') && !held.startsWith(typesHeader)) {
|
|
211
|
+
throw new Refusal(`${named(at)} was not written by \`mithaq generate screens\` and generating would overwrite it. Move it or delete it.`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
for (const [index, [at, source]] of wanted.entries()) {
|
|
215
|
+
if (before[index] === source)
|
|
216
|
+
continue;
|
|
217
|
+
stale.push(named(at));
|
|
218
|
+
if (check)
|
|
219
|
+
continue;
|
|
220
|
+
await mkdir(new URL('./', at), { recursive: true });
|
|
221
|
+
await writeFile(at, source);
|
|
222
|
+
}
|
|
223
|
+
// Only a file carrying the header is ever deleted.
|
|
224
|
+
const kept = new Set(wanted.map(([at]) => fileURLToPath(at)));
|
|
225
|
+
const held = await readdir(into, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
226
|
+
for (const entry of held.filter((one) => one.isFile()).map((one) => join(one.parentPath, one.name)).sort()) {
|
|
227
|
+
if (kept.has(entry) || !(await readFile(entry, 'utf8')).startsWith(typesHeader))
|
|
228
|
+
continue;
|
|
229
|
+
stale.push(named(pathToFileURL(entry)));
|
|
230
|
+
if (!check)
|
|
231
|
+
await rm(entry);
|
|
232
|
+
}
|
|
233
|
+
return stale;
|
|
234
|
+
};
|
|
235
|
+
/** The command, given the project's own root, answering the code it exits with. */
|
|
236
|
+
export const run = async (project, check) => {
|
|
237
|
+
const source = `import { asas } from ${JSON.stringify(new URL('src/asas.ts', project).href)}; const { locales, generate } = await import(${JSON.stringify(new URL('src/mithaq.ts', project).href)}).catch(() => ({})); const own = Object.entries(asas.targets ?? {}).filter(([, target]) => target?.capability === 'screen'); process.stdout.write(JSON.stringify({ locales: locales ?? [], into: generate?.screens, screens: Object.fromEntries(own.map(([name, target]) => [name, target.client])) }))`;
|
|
238
|
+
const { stdout } = await exec(process.execPath, ['--input-type=module', '-e', source], { cwd: fileURLToPath(project), maxBuffer: 16 * 1024 * 1024 });
|
|
239
|
+
const { locales, into, screens } = JSON.parse(stdout);
|
|
240
|
+
// A build with no screen writes nothing, so it needs no folder named.
|
|
241
|
+
const folder = (value) => (typeof value === 'string' && value.endsWith('/') ? value : undefined);
|
|
242
|
+
if (!folder(into) && (typeof into !== 'object' || into === null) && Object.keys(screens).length) {
|
|
243
|
+
throw new Refusal('`mithaq generate screens` writes under the folder `generate.screens` names in src/mithaq.ts, and it names none. Write `generate: { screens: \'src/routes/(screens)/\' }`.');
|
|
244
|
+
}
|
|
245
|
+
// Every folder named is swept even when it holds no screen now, so a removed screen leaves no route behind.
|
|
246
|
+
const byFolder = new Map();
|
|
247
|
+
for (const at of folder(into) ? [into] : Object.values(into ?? {}))
|
|
248
|
+
if (folder(at))
|
|
249
|
+
byFolder.set(at, {});
|
|
250
|
+
for (const [name, client] of Object.entries(screens)) {
|
|
251
|
+
// `false` is a project saying no app of its own serves this screen, so nothing is written for it.
|
|
252
|
+
if (!folder(into) && into[name] === false)
|
|
253
|
+
continue;
|
|
254
|
+
const at = folder(into) ?? folder(into[name]);
|
|
255
|
+
if (!at)
|
|
256
|
+
throw new Refusal(`generate.screens names no folder for the screen ${name}. Name one: \`screens: { ${name}: 'src/routes/(screens)/' }\`.`);
|
|
257
|
+
byFolder.set(at, { ...byFolder.get(at), [name]: client });
|
|
258
|
+
}
|
|
259
|
+
const stale = [];
|
|
260
|
+
for (const [at, own] of byFolder)
|
|
261
|
+
stale.push(...(await generateScreens({ root: project, screens: own, into: new URL(at, project), locales, check })));
|
|
262
|
+
if (check && stale.length) {
|
|
263
|
+
console.error(`${stale.join(', ')} ${stale.length === 1 ? 'is' : 'are'} not what a run would write. Run \`mithaq generate screens\`.`);
|
|
264
|
+
return 1;
|
|
265
|
+
}
|
|
266
|
+
console.log(stale.length ? `Wrote ${stale.join(', ')}.` : 'Screens are up to date.');
|
|
267
|
+
return 0;
|
|
268
|
+
};
|