@drhomet/mithaq 0.0.0-stage → 0.1.1
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/payload.txt +22 -0
- package/LICENSES/ponytail.txt +21 -0
- package/dist/asas/actor.d.ts +96 -0
- package/dist/asas/actor.js +114 -0
- package/dist/asas/build.d.ts +231 -0
- package/dist/asas/build.js +787 -0
- package/dist/asas/cli/mithaq.d.ts +2 -0
- package/dist/asas/cli/mithaq.js +145 -0
- package/dist/asas/cli/upgrade.d.ts +20 -0
- package/dist/asas/cli/upgrade.js +147 -0
- package/dist/asas/http/fetch.d.ts +23 -0
- package/dist/asas/http/fetch.js +115 -0
- package/dist/asas/http/orpc.d.ts +43 -0
- package/dist/asas/http/orpc.js +135 -0
- package/dist/asas/runner/bulk.d.ts +20 -0
- package/dist/asas/runner/bulk.js +191 -0
- package/dist/asas/runner/errors.d.ts +60 -0
- package/dist/asas/runner/errors.js +59 -0
- package/dist/asas/runner/read.d.ts +68 -0
- package/dist/asas/runner/read.js +579 -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 +400 -0
- package/dist/asas/runner/steps.js +271 -0
- package/dist/asas/runner/tenant.d.ts +54 -0
- package/dist/asas/runner/tenant.js +213 -0
- package/dist/asas/runner/write.d.ts +444 -0
- package/dist/asas/runner/write.js +791 -0
- package/dist/auth/better-auth/access.d.ts +15 -0
- package/dist/auth/better-auth/access.js +71 -0
- package/dist/auth/better-auth/generate.d.ts +29 -0
- package/dist/auth/better-auth/generate.js +310 -0
- package/dist/auth/better-auth/index.d.ts +92 -0
- package/dist/auth/better-auth/index.js +81 -0
- package/dist/auth/better-auth/tenant.d.ts +18 -0
- package/dist/auth/better-auth/tenant.js +26 -0
- package/dist/auth/local/index.d.ts +285 -0
- package/dist/auth/local/index.js +736 -0
- package/dist/base/decimal.d.ts +9 -0
- package/dist/base/decimal.js +16 -0
- package/dist/base/fields.d.ts +123 -0
- package/dist/base/fields.js +51 -0
- package/dist/base/index.d.ts +46 -0
- package/dist/base/index.js +5 -0
- package/dist/base/kinds.d.ts +54 -0
- package/dist/base/kinds.js +78 -0
- package/dist/base/rules.d.ts +73 -0
- package/dist/base/rules.js +80 -0
- package/dist/drizzle/index.d.ts +438 -0
- package/dist/drizzle/index.js +832 -0
- package/dist/drizzle/kinds.d.ts +27 -0
- package/dist/drizzle/kinds.js +41 -0
- package/dist/drizzle/migrate.d.ts +133 -0
- package/dist/drizzle/migrate.js +1285 -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 +45 -0
- package/dist/index.js +36 -0
- package/dist/mithaq/define.d.ts +288 -0
- package/dist/mithaq/define.js +43 -0
- package/dist/mithaq/generate-messages.d.ts +38 -0
- package/dist/mithaq/generate-messages.js +234 -0
- package/dist/mithaq/language.d.ts +335 -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/messages.d.ts +55 -0
- package/dist/mithaq/messages.js +106 -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 +954 -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 +358 -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 +27 -0
- package/dist/rich-text/index.js +58 -0
- package/dist/rich-text/rich-text.d.ts +159 -0
- package/dist/rich-text/rich-text.js +372 -0
- package/dist/screen/client.d.ts +153 -0
- package/dist/screen/client.js +68 -0
- package/dist/screen/generate.d.ts +14 -0
- package/dist/screen/generate.js +226 -0
- package/dist/screen/index.d.ts +479 -0
- package/dist/screen/index.js +645 -0
- package/dist/screen/react.d.ts +95 -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 +43 -0
- package/dist/storage/index.js +44 -0
- package/dist/storage/storage.d.ts +384 -0
- package/dist/storage/storage.js +405 -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/zod/index.d.ts +184 -0
- package/dist/zod/index.js +121 -0
- package/dist/zod/kinds.d.ts +34 -0
- package/dist/zod/kinds.js +49 -0
- package/dist/zod/rules.d.ts +117 -0
- package/dist/zod/rules.js +67 -0
- package/package.json +63 -4
- package/r/mithaq-cells.json +20 -0
- package/r/mithaq-controls.json +37 -0
- package/r/mithaq-directional-icon.json +15 -0
- package/r/mithaq-expected-updated-at.json +15 -0
- package/r/mithaq-field-shell.json +18 -0
- package/r/mithaq-filter-operators.json +15 -0
- package/r/mithaq-form.json +33 -0
- package/r/mithaq-list-columns.json +23 -0
- package/r/mithaq-list-filters.json +24 -0
- package/r/mithaq-list-pagination.json +24 -0
- package/r/mithaq-list-query.json +18 -0
- package/r/mithaq-list-selection.json +19 -0
- package/r/mithaq-list.json +33 -0
- package/r/mithaq-nav.json +24 -0
- package/r/mithaq-nodes.json +34 -0
- package/r/mithaq-relationship-cell.json +20 -0
- package/r/mithaq-relationship-control.json +20 -0
- package/r/mithaq-rich-text.json +40 -0
- package/r/mithaq-screens-host.json +96 -0
- package/r/mithaq-storage.json +44 -0
- package/r/mithaq-upload.json +29 -0
- package/r/registry.json +550 -0
- package/README.md +0 -3
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { join, relative } 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, linked }) => {
|
|
67
|
+
const byPackage = new Map();
|
|
68
|
+
for (const key of new Set(referencesIn(client).map((reference) => reference.slice(0, reference.indexOf('#'))))) {
|
|
69
|
+
const name = key.startsWith('./') ? '' : key.slice(0, key.indexOf(':'));
|
|
70
|
+
byPackage.set(name, [...(byPackage.get(name) ?? []), name ? key.slice(name.length + 1) : key]);
|
|
71
|
+
}
|
|
72
|
+
const glob = (files, base) => `import.meta.glob([${files.sort().map(sourceString).join(', ')}], { base: ${sourceString(base)}${base === '/' ? '' : ', exhaustive: true'} })`;
|
|
73
|
+
const bare = byPackage.get('') ?? [];
|
|
74
|
+
// `screen.json` prints a path decoded, which `new URL` would misread at a `%` or a `?`.
|
|
75
|
+
const base = bare.length && bare.every((file) => existsSync(join(fileURLToPath(app), file))) ? '/' : linked;
|
|
76
|
+
const at = base === '/' ? app : root;
|
|
77
|
+
const missing = bare.filter((file) => !existsSync(join(fileURLToPath(at), file)));
|
|
78
|
+
if (missing.length) {
|
|
79
|
+
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.`);
|
|
80
|
+
}
|
|
81
|
+
const own = glob(bare, base);
|
|
82
|
+
const named = [...byPackage.keys()].filter(Boolean).sort();
|
|
83
|
+
if (!named.length) {
|
|
84
|
+
return `${typesHeader}
|
|
85
|
+
/// <reference types="vite/client" />
|
|
86
|
+
// Every file the \`${screen}\` screen's references name, each loaded when a page draws it.
|
|
87
|
+
export const files = ${own}
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
return `${typesHeader}
|
|
91
|
+
/// <reference types="vite/client" />
|
|
92
|
+
// Every file the \`${screen}\` screen's references name, each loaded when a page draws it, keyed as the reference prints it.
|
|
93
|
+
const named = (name: string, files: Record<string, () => Promise<unknown>>) => Object.fromEntries(Object.entries(files).map(([file, load]) => [\`\${name}:\${file}\`, load]))
|
|
94
|
+
export const files = {
|
|
95
|
+
${bare.length ? ` ...${own},\n` : ''}${named.map((name) => ` ...named(${sourceString(name)}, ${glob(byPackage.get(name), name === appName ? '/' : `/node_modules/${name}/`)}),\n`).join('')}}
|
|
96
|
+
`;
|
|
97
|
+
};
|
|
98
|
+
const routesOf = (screen, client, prefix) => {
|
|
99
|
+
const segments = client.path.split('/').filter(Boolean);
|
|
100
|
+
const base = segments.join('.');
|
|
101
|
+
const id = `${prefix}${segments.join('/')}`;
|
|
102
|
+
const name = sourceString(screen);
|
|
103
|
+
const imports = (names, host) => `${typesHeader}
|
|
104
|
+
import { ${names} } from '@tanstack/react-router'
|
|
105
|
+
import { ${host} } from '@/screens.tsx'
|
|
106
|
+
import { files } from './${screen}/files.ts'
|
|
107
|
+
`;
|
|
108
|
+
return [
|
|
109
|
+
[`${base}.tsx`, `${imports('Outlet, createFileRoute', 'ScreenFrame, frameLoader')}
|
|
110
|
+
export const Route = createFileRoute('${id}')({
|
|
111
|
+
loader: ({ location }) => frameLoader({ screen: ${name}, files, href: location.href }),
|
|
112
|
+
component: () => (
|
|
113
|
+
<ScreenFrame files={files} data={Route.useLoaderData()}>
|
|
114
|
+
<Outlet />
|
|
115
|
+
</ScreenFrame>
|
|
116
|
+
),
|
|
117
|
+
})
|
|
118
|
+
`],
|
|
119
|
+
[`${base}.index.tsx`, `${imports('createFileRoute', 'ScreenPageView, askedFor, pageHead, pageLoader')}
|
|
120
|
+
export const Route = createFileRoute('${id}/')({
|
|
121
|
+
loader: ({ location }) => pageLoader({ screen: ${name}, files, path: '', query: {}, href: location.href }),
|
|
122
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
123
|
+
component: () => <ScreenPageView files={files} data={Route.useLoaderData()} query={askedFor({})} onQuery={() => {}} />,
|
|
124
|
+
})
|
|
125
|
+
`],
|
|
126
|
+
[`${base}.$.tsx`, `${imports('createFileRoute', 'ScreenNotFound, ScreenPageView, askedFor, pageHead, pageLoader')}
|
|
127
|
+
export const Route = createFileRoute('${id}/$')({
|
|
128
|
+
validateSearch: askedFor,
|
|
129
|
+
loader: ({ location, params }) => pageLoader({ screen: ${name}, files, path: params._splat ?? '', query: location.search, href: location.href }),
|
|
130
|
+
head: ({ loaderData, match }) => pageHead(loaderData, match.context),
|
|
131
|
+
notFoundComponent: () => <ScreenNotFound files={files} screen={${name}} />,
|
|
132
|
+
component: Page,
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
function Page() {
|
|
136
|
+
const navigate = Route.useNavigate()
|
|
137
|
+
return <ScreenPageView files={files} data={Route.useLoaderData()} query={Route.useSearch()} onQuery={(search) => void navigate({ search })} />
|
|
138
|
+
}
|
|
139
|
+
`],
|
|
140
|
+
];
|
|
141
|
+
};
|
|
142
|
+
/**
|
|
143
|
+
* Writes each screen's `screen.json`, `types.ts`, `files.ts` and route files under `into`, and answers the
|
|
144
|
+
* stale files. With `check` it writes nothing. `into` must sit under a TanStack Router `routes` folder.
|
|
145
|
+
*/
|
|
146
|
+
export const generateScreens = async ({ root, screens, into, locales = [], check = false, }) => {
|
|
147
|
+
const stale = [];
|
|
148
|
+
const named = (at) => relative(fileURLToPath(root), fileURLToPath(at)).replaceAll('\\', '/');
|
|
149
|
+
const routesAt = into.pathname.lastIndexOf('/routes/');
|
|
150
|
+
if (routesAt < 0 && Object.keys(screens).length) {
|
|
151
|
+
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.`);
|
|
152
|
+
}
|
|
153
|
+
const prefix = decodeURIComponent(into.pathname.slice(routesAt + '/routes'.length));
|
|
154
|
+
// The app is the package holding `into`, and Vite's root is its folder.
|
|
155
|
+
const app = packageOf(into) ?? root;
|
|
156
|
+
const linked = app.href === root.href ? '/' : `/node_modules/${JSON.parse(await readFile(new URL('package.json', root), 'utf8')).name}/`;
|
|
157
|
+
const appName = JSON.parse(await readFile(new URL('package.json', app), 'utf8').catch(() => '{}')).name;
|
|
158
|
+
const wanted = Object.entries(screens).flatMap(([key, client]) => {
|
|
159
|
+
const from = new URL(`${key}/`, into);
|
|
160
|
+
return [
|
|
161
|
+
[new URL('screen.json', from), `${JSON.stringify(client, null, 2)}\n`],
|
|
162
|
+
[new URL('types.ts', from), typesOf(client, locales)],
|
|
163
|
+
[new URL('files.ts', from), globOf({ screen: key, client, root, app, appName, linked })],
|
|
164
|
+
...routesOf(key, client, prefix).map(([file, source]) => [new URL(file, into), source]),
|
|
165
|
+
];
|
|
166
|
+
});
|
|
167
|
+
const before = await Promise.all(wanted.map(([at]) => readFile(at, 'utf8').catch(() => undefined)));
|
|
168
|
+
// A file of the project's own is never overwritten, and nothing is written until every file has been asked.
|
|
169
|
+
wanted.forEach(([at], index) => {
|
|
170
|
+
const held = before[index];
|
|
171
|
+
if (held !== undefined && !at.pathname.endsWith('.json') && !held.startsWith(typesHeader)) {
|
|
172
|
+
throw new Refusal(`${named(at)} was not written by \`mithaq generate screens\` and generating would overwrite it. Move it or delete it.`);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
for (const [index, [at, source]] of wanted.entries()) {
|
|
176
|
+
if (before[index] === source)
|
|
177
|
+
continue;
|
|
178
|
+
stale.push(named(at));
|
|
179
|
+
if (check)
|
|
180
|
+
continue;
|
|
181
|
+
await mkdir(new URL('./', at), { recursive: true });
|
|
182
|
+
await writeFile(at, source);
|
|
183
|
+
}
|
|
184
|
+
// Only a file carrying the header is ever deleted.
|
|
185
|
+
const kept = new Set(wanted.map(([at]) => fileURLToPath(at)));
|
|
186
|
+
const held = await readdir(into, { recursive: true, withFileTypes: true }).catch(() => []);
|
|
187
|
+
for (const entry of held.filter((one) => one.isFile()).map((one) => join(one.parentPath, one.name)).sort()) {
|
|
188
|
+
if (kept.has(entry) || !(await readFile(entry, 'utf8')).startsWith(typesHeader))
|
|
189
|
+
continue;
|
|
190
|
+
stale.push(named(pathToFileURL(entry)));
|
|
191
|
+
if (!check)
|
|
192
|
+
await rm(entry);
|
|
193
|
+
}
|
|
194
|
+
return stale;
|
|
195
|
+
};
|
|
196
|
+
/** The command, given the project's own root, answering the code it exits with. */
|
|
197
|
+
export const run = async (project, check) => {
|
|
198
|
+
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])) }))`;
|
|
199
|
+
const { stdout } = await exec(process.execPath, ['--input-type=module', '-e', source], { cwd: fileURLToPath(project), maxBuffer: 16 * 1024 * 1024 });
|
|
200
|
+
const { locales, into, screens } = JSON.parse(stdout);
|
|
201
|
+
// A build with no screen writes nothing, so it needs no folder named.
|
|
202
|
+
const folder = (value) => (typeof value === 'string' && value.endsWith('/') ? value : undefined);
|
|
203
|
+
if (!folder(into) && (typeof into !== 'object' || into === null) && Object.keys(screens).length) {
|
|
204
|
+
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)/\' }`.');
|
|
205
|
+
}
|
|
206
|
+
// Every folder named is swept even when it holds no screen now, so a removed screen leaves no route behind.
|
|
207
|
+
const byFolder = new Map();
|
|
208
|
+
for (const at of folder(into) ? [into] : Object.values(into ?? {}))
|
|
209
|
+
if (folder(at))
|
|
210
|
+
byFolder.set(at, {});
|
|
211
|
+
for (const [name, client] of Object.entries(screens)) {
|
|
212
|
+
const at = folder(into) ?? folder(into[name]);
|
|
213
|
+
if (!at)
|
|
214
|
+
throw new Refusal(`generate.screens names no folder for the screen ${name}. Name one: \`screens: { ${name}: 'src/routes/(screens)/' }\`.`);
|
|
215
|
+
byFolder.set(at, { ...byFolder.get(at), [name]: client });
|
|
216
|
+
}
|
|
217
|
+
const stale = [];
|
|
218
|
+
for (const [at, own] of byFolder)
|
|
219
|
+
stale.push(...(await generateScreens({ root: project, screens: own, into: new URL(at, project), locales, check })));
|
|
220
|
+
if (check && stale.length) {
|
|
221
|
+
console.error(`${stale.join(', ')} ${stale.length === 1 ? 'is' : 'are'} not what a run would write. Run \`mithaq generate screens\`.`);
|
|
222
|
+
return 1;
|
|
223
|
+
}
|
|
224
|
+
console.log(stale.length ? `Wrote ${stale.join(', ')}.` : 'Screens are up to date.');
|
|
225
|
+
return 0;
|
|
226
|
+
};
|