create-ultimabase 0.0.0-stage → 0.1.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/CHANGELOG.md +7 -0
- package/README.md +4 -2
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +4 -0
- package/dist/main.d.ts +16 -0
- package/dist/main.d.ts.map +1 -0
- package/dist/main.js +79 -0
- package/dist/starter.d.ts +13 -0
- package/dist/starter.d.ts.map +1 -0
- package/dist/starter.js +68 -0
- package/package.json +37 -3
- package/src/cli.ts +5 -0
- package/src/main.ts +103 -0
- package/src/starter.ts +89 -0
- package/template/gitignore +1 -0
- package/template/src/app.test.ts +20 -0
- package/template/src/app.ts +16 -0
- package/template/src/models.ts +11 -0
- package/template/tsconfig.json +17 -0
- package/template/ultimabase.config.ts +2 -0
package/CHANGELOG.md
ADDED
package/README.md
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
-
#
|
|
1
|
+
# create-ultimabase
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Writes a new UltimaBase application, ready to start: its models, a test and its server. Run it with `npm create ultimabase`.
|
|
4
|
+
|
|
5
|
+
Documentation: [Quickstart](https://ultimabase.dev/docs/quickstart).
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
|
package/dist/cli.js
ADDED
package/dist/main.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type Io = {
|
|
2
|
+
readonly cwd: string;
|
|
3
|
+
/** `npm_config_user_agent`, which names the package manager that ran it. */
|
|
4
|
+
readonly userAgent: string | undefined;
|
|
5
|
+
/** Asks in a terminal; `undefined` elsewhere. */
|
|
6
|
+
readonly ask: ((question: string) => Promise<string>) | undefined;
|
|
7
|
+
readonly print: (text: string) => void;
|
|
8
|
+
readonly fail: (text: string) => void;
|
|
9
|
+
};
|
|
10
|
+
/** A question asked on `input` and `output`, a terminal. */
|
|
11
|
+
export declare const terminal: (input: NodeJS.ReadableStream, output: NodeJS.WritableStream) => (question: string) => Promise<string>;
|
|
12
|
+
/** The process's directory, package manager, terminal and output. */
|
|
13
|
+
export declare function processIo(): Io;
|
|
14
|
+
/** Writes the starter; the process's exit code. */
|
|
15
|
+
export declare function main(args: readonly string[], io: Io): Promise<number>;
|
|
16
|
+
//# sourceMappingURL=main.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,EAAE,GAAG;IAChB,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAA;IACpB,4EAA4E;IAC5E,QAAQ,CAAC,SAAS,EAAE,MAAM,GAAG,SAAS,CAAA;IACtC,iDAAiD;IACjD,QAAQ,CAAC,GAAG,EAAE,CAAC,CAAC,QAAQ,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC,GAAG,SAAS,CAAA;IACjE,QAAQ,CAAC,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;IACtC,QAAQ,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAA;CACrC,CAAA;AAmBD,4DAA4D;AAC5D,eAAO,MAAM,QAAQ,UACZ,MAAM,CAAC,cAAc,UAAU,MAAM,CAAC,cAAc,gBAC3C,MAAM,KAAG,OAAO,CAAC,MAAM,CAOvC,CAAA;AAEF,qEAAqE;AACrE,wBAAgB,SAAS,IAAI,EAAE,CAa9B;AAYD,mDAAmD;AACnD,wBAAsB,IAAI,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA4B3E"}
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// `npm create ultimabase [directory]` (D-130): writes the starter in an empty
|
|
2
|
+
// directory, installs nothing, and prints the next commands.
|
|
3
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
4
|
+
import { createInterface } from 'node:readline/promises';
|
|
5
|
+
import { basename, relative, resolve } from 'node:path';
|
|
6
|
+
import { write } from './starter.js';
|
|
7
|
+
const usage = 'Usage: npm create ultimabase [directory]';
|
|
8
|
+
const fallback = 'my-app';
|
|
9
|
+
/** npm's rule for a new package's name. */
|
|
10
|
+
const named = (name) => name.length <= 214 && /^[a-z0-9~-][a-z0-9._~-]*$/u.test(name);
|
|
11
|
+
const managerOf = (userAgent) => {
|
|
12
|
+
const name = userAgent?.split('/')[0];
|
|
13
|
+
return name === 'yarn' || name === 'pnpm' ? name : 'npm';
|
|
14
|
+
};
|
|
15
|
+
const steps = {
|
|
16
|
+
npm: ['npm install', 'npm run dev'],
|
|
17
|
+
yarn: ['yarn', 'yarn dev'],
|
|
18
|
+
pnpm: ['pnpm install', 'pnpm dev'],
|
|
19
|
+
};
|
|
20
|
+
/** A question asked on `input` and `output`, a terminal. */
|
|
21
|
+
export const terminal = (input, output) => async (question) => {
|
|
22
|
+
const line = createInterface({ input, output });
|
|
23
|
+
try {
|
|
24
|
+
return await line.question(question);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
line.close();
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
/** The process's directory, package manager, terminal and output. */
|
|
31
|
+
export function processIo() {
|
|
32
|
+
const tty = process.stdin.isTTY && process.stdout.isTTY;
|
|
33
|
+
return {
|
|
34
|
+
cwd: process.cwd(),
|
|
35
|
+
userAgent: process.env['npm_config_user_agent'],
|
|
36
|
+
ask: tty ? terminal(process.stdin, process.stdout) : undefined,
|
|
37
|
+
print: (text) => {
|
|
38
|
+
console.log(text);
|
|
39
|
+
},
|
|
40
|
+
fail: (text) => {
|
|
41
|
+
console.error(text);
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
async function directoryOf(io) {
|
|
46
|
+
if (io.ask === undefined)
|
|
47
|
+
return fallback;
|
|
48
|
+
const answer = (await io.ask(`? Project name: (${fallback}) `)).trim();
|
|
49
|
+
return answer === '' ? fallback : answer;
|
|
50
|
+
}
|
|
51
|
+
const empty = (target) => !existsSync(target) ||
|
|
52
|
+
(statSync(target).isDirectory() && readdirSync(target).length === 0);
|
|
53
|
+
/** Writes the starter; the process's exit code. */
|
|
54
|
+
export async function main(args, io) {
|
|
55
|
+
const [given, ...more] = args;
|
|
56
|
+
if (more.length > 0 || given?.startsWith('-') === true) {
|
|
57
|
+
io.fail(usage);
|
|
58
|
+
return 1;
|
|
59
|
+
}
|
|
60
|
+
const directory = given ?? (await directoryOf(io));
|
|
61
|
+
const target = resolve(io.cwd, directory);
|
|
62
|
+
const name = basename(target);
|
|
63
|
+
if (!named(name)) {
|
|
64
|
+
io.fail(`${name} cannot name a package: use lowercase letters, digits, '-', '.' and '_'.`);
|
|
65
|
+
return 1;
|
|
66
|
+
}
|
|
67
|
+
if (!empty(target)) {
|
|
68
|
+
io.fail(`${directory} is not an empty directory.`);
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
const manager = managerOf(io.userAgent);
|
|
72
|
+
write(target, name, manager);
|
|
73
|
+
const cd = relative(io.cwd, target) === ''
|
|
74
|
+
? []
|
|
75
|
+
: [`cd ${/\s/u.test(directory) ? `"${directory}"` : directory}`];
|
|
76
|
+
const next = [...cd, ...steps[manager]].map((step) => ` ${step}`);
|
|
77
|
+
io.print(`\nCreated ${name}. Next:\n\n${next.join('\n')}\n`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type Manager = 'npm' | 'yarn' | 'pnpm';
|
|
2
|
+
/** The packages the starter runs beside UltimaBase's, as the repository tests them. */
|
|
3
|
+
export declare const others: {
|
|
4
|
+
readonly '@types/node': '^24.19.2';
|
|
5
|
+
readonly graphql: '^17.0.2';
|
|
6
|
+
readonly vite: '^8.3.4';
|
|
7
|
+
readonly vitest: '^5.0.5';
|
|
8
|
+
};
|
|
9
|
+
/** `create-ultimabase`'s version, which the official packages share (D-60). */
|
|
10
|
+
export declare function ownVersion(): string;
|
|
11
|
+
/** Writes the starter named `name` in `target`, an empty or missing directory. */
|
|
12
|
+
export declare function write(target: string, name: string, manager: Manager): void;
|
|
13
|
+
//# sourceMappingURL=starter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"starter.d.ts","sourceRoot":"","sources":["../src/starter.ts"],"names":[],"mappings":"AAYA,MAAM,MAAM,OAAO,GAAG,KAAK,GAAG,MAAM,GAAG,MAAM,CAAA;AAI7C,uFAAuF;AACvF,eAAO,MAAM,MAAM;aAClB,aAAa,EAAE,UAAU;aACzB,OAAO,EAAE,SAAS;aAClB,IAAI,EAAE,QAAQ;aACd,MAAM,EAAE,QAAQ;CACP,CAAA;AAEV,+EAA+E;AAC/E,wBAAgB,UAAU,IAAI,MAAM,CAKnC;AAgDD,kFAAkF;AAClF,wBAAgB,KAAK,CAAC,MAAM,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,IAAI,CAS1E"}
|
package/dist/starter.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// The starter's files (D-130): the template as it is, its `gitignore` as
|
|
2
|
+
// `.gitignore`, and the package.json and README that name the application.
|
|
3
|
+
import { cpSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
const template = fileURLToPath(new URL('../template', import.meta.url));
|
|
7
|
+
/** The packages the starter runs beside UltimaBase's, as the repository tests them. */
|
|
8
|
+
export const others = {
|
|
9
|
+
'@types/node': '^24.19.2',
|
|
10
|
+
graphql: '^17.0.2',
|
|
11
|
+
vite: '^8.3.4',
|
|
12
|
+
vitest: '^5.0.5',
|
|
13
|
+
};
|
|
14
|
+
/** `create-ultimabase`'s version, which the official packages share (D-60). */
|
|
15
|
+
export function ownVersion() {
|
|
16
|
+
const manifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
17
|
+
return String(Reflect.get(Object(manifest), 'version'));
|
|
18
|
+
}
|
|
19
|
+
const manifestOf = (name) => {
|
|
20
|
+
const ours = `^${ownVersion()}`;
|
|
21
|
+
return {
|
|
22
|
+
name,
|
|
23
|
+
private: true,
|
|
24
|
+
type: 'module',
|
|
25
|
+
engines: { node: '>=24.7.0' },
|
|
26
|
+
scripts: {
|
|
27
|
+
dev: 'ultimabase dev',
|
|
28
|
+
start: 'ultimabase start',
|
|
29
|
+
test: 'vitest',
|
|
30
|
+
},
|
|
31
|
+
dependencies: {
|
|
32
|
+
'@ultimabase/graphql': ours,
|
|
33
|
+
'@ultimabase/http': ours,
|
|
34
|
+
graphql: others.graphql,
|
|
35
|
+
ultimabase: ours,
|
|
36
|
+
},
|
|
37
|
+
devDependencies: {
|
|
38
|
+
'@types/node': others['@types/node'],
|
|
39
|
+
'@ultimabase/testing': ours,
|
|
40
|
+
vite: others.vite,
|
|
41
|
+
vitest: others.vitest,
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
/** How `manager` runs a script other than `start` and `test`. */
|
|
46
|
+
const run = (manager) => manager === 'npm' ? 'npm run' : manager;
|
|
47
|
+
const readme = (name, manager) => `# ${name}
|
|
48
|
+
|
|
49
|
+
An UltimaBase application. Node runs its TypeScript as it is written.
|
|
50
|
+
|
|
51
|
+
- \`${run(manager)} dev\` serves it on http://localhost:3000, GraphQL on
|
|
52
|
+
\`/graphql\`, and restarts it when a file changes; \`PORT\` sets another
|
|
53
|
+
port.
|
|
54
|
+
- \`${manager} start\` serves it in production.
|
|
55
|
+
- \`${manager} test\` runs its tests, each on a new in-memory database.
|
|
56
|
+
|
|
57
|
+
\`src/models.ts\` declares the models, \`src/app.ts\` the database and the
|
|
58
|
+
plugins, and \`ultimabase.config.ts\` names the application the
|
|
59
|
+
\`ultimabase\` command runs.
|
|
60
|
+
`;
|
|
61
|
+
/** Writes the starter named `name` in `target`, an empty or missing directory. */
|
|
62
|
+
export function write(target, name, manager) {
|
|
63
|
+
mkdirSync(target, { recursive: true });
|
|
64
|
+
cpSync(template, target, { recursive: true });
|
|
65
|
+
renameSync(join(target, 'gitignore'), join(target, '.gitignore'));
|
|
66
|
+
writeFileSync(join(target, 'package.json'), `${JSON.stringify(manifestOf(name), undefined, '\t')}\n`);
|
|
67
|
+
writeFileSync(join(target, 'README.md'), readme(name, manager));
|
|
68
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-ultimabase",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Writes a new UltimaBase application: npm create ultimabase.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "Apache-2.0",
|
|
7
|
+
"homepage": "https://ultimabase.dev/docs/quickstart",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/MrEkinox/ultimabase.git",
|
|
11
|
+
"directory": "packages/create-ultimabase"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=24.7.0"
|
|
15
|
+
},
|
|
16
|
+
"bin": "./dist/cli.js",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"src",
|
|
20
|
+
"template",
|
|
21
|
+
"!src/**/*.test.ts",
|
|
22
|
+
"!src/test"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"prepack": "rm -rf dist && yarn run -T tsc --project tsconfig.build.json"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"bin": "./dist/cli.js"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/node": "24.19.2",
|
|
32
|
+
"@ultimabase/graphql": "0.1.0",
|
|
33
|
+
"@ultimabase/http": "0.1.0",
|
|
34
|
+
"@ultimabase/testing": "0.1.0",
|
|
35
|
+
"graphql": "17.0.2",
|
|
36
|
+
"ultimabase": "0.1.0",
|
|
37
|
+
"vite": "8.3.4",
|
|
38
|
+
"vitest": "5.0.5"
|
|
39
|
+
}
|
|
6
40
|
}
|
package/src/cli.ts
ADDED
package/src/main.ts
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// `npm create ultimabase [directory]` (D-130): writes the starter in an empty
|
|
2
|
+
// directory, installs nothing, and prints the next commands.
|
|
3
|
+
import { existsSync, readdirSync, statSync } from 'node:fs'
|
|
4
|
+
import { createInterface } from 'node:readline/promises'
|
|
5
|
+
import { basename, relative, resolve } from 'node:path'
|
|
6
|
+
import type { Manager } from './starter.ts'
|
|
7
|
+
import { write } from './starter.ts'
|
|
8
|
+
|
|
9
|
+
export type Io = {
|
|
10
|
+
readonly cwd: string
|
|
11
|
+
/** `npm_config_user_agent`, which names the package manager that ran it. */
|
|
12
|
+
readonly userAgent: string | undefined
|
|
13
|
+
/** Asks in a terminal; `undefined` elsewhere. */
|
|
14
|
+
readonly ask: ((question: string) => Promise<string>) | undefined
|
|
15
|
+
readonly print: (text: string) => void
|
|
16
|
+
readonly fail: (text: string) => void
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const usage = 'Usage: npm create ultimabase [directory]'
|
|
20
|
+
const fallback = 'my-app'
|
|
21
|
+
|
|
22
|
+
/** npm's rule for a new package's name. */
|
|
23
|
+
const named = (name: string): boolean =>
|
|
24
|
+
name.length <= 214 && /^[a-z0-9~-][a-z0-9._~-]*$/u.test(name)
|
|
25
|
+
|
|
26
|
+
const managerOf = (userAgent: string | undefined): Manager => {
|
|
27
|
+
const name = userAgent?.split('/')[0]
|
|
28
|
+
return name === 'yarn' || name === 'pnpm' ? name : 'npm'
|
|
29
|
+
}
|
|
30
|
+
const steps: Record<Manager, readonly string[]> = {
|
|
31
|
+
npm: ['npm install', 'npm run dev'],
|
|
32
|
+
yarn: ['yarn', 'yarn dev'],
|
|
33
|
+
pnpm: ['pnpm install', 'pnpm dev'],
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A question asked on `input` and `output`, a terminal. */
|
|
37
|
+
export const terminal =
|
|
38
|
+
(input: NodeJS.ReadableStream, output: NodeJS.WritableStream) =>
|
|
39
|
+
async (question: string): Promise<string> => {
|
|
40
|
+
const line = createInterface({ input, output })
|
|
41
|
+
try {
|
|
42
|
+
return await line.question(question)
|
|
43
|
+
} finally {
|
|
44
|
+
line.close()
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The process's directory, package manager, terminal and output. */
|
|
49
|
+
export function processIo(): Io {
|
|
50
|
+
const tty = process.stdin.isTTY && process.stdout.isTTY
|
|
51
|
+
return {
|
|
52
|
+
cwd: process.cwd(),
|
|
53
|
+
userAgent: process.env['npm_config_user_agent'],
|
|
54
|
+
ask: tty ? terminal(process.stdin, process.stdout) : undefined,
|
|
55
|
+
print: (text) => {
|
|
56
|
+
console.log(text)
|
|
57
|
+
},
|
|
58
|
+
fail: (text) => {
|
|
59
|
+
console.error(text)
|
|
60
|
+
},
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function directoryOf(io: Io): Promise<string> {
|
|
65
|
+
if (io.ask === undefined) return fallback
|
|
66
|
+
const answer = (await io.ask(`? Project name: (${fallback}) `)).trim()
|
|
67
|
+
return answer === '' ? fallback : answer
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const empty = (target: string): boolean =>
|
|
71
|
+
!existsSync(target) ||
|
|
72
|
+
(statSync(target).isDirectory() && readdirSync(target).length === 0)
|
|
73
|
+
|
|
74
|
+
/** Writes the starter; the process's exit code. */
|
|
75
|
+
export async function main(args: readonly string[], io: Io): Promise<number> {
|
|
76
|
+
const [given, ...more] = args
|
|
77
|
+
if (more.length > 0 || given?.startsWith('-') === true) {
|
|
78
|
+
io.fail(usage)
|
|
79
|
+
return 1
|
|
80
|
+
}
|
|
81
|
+
const directory = given ?? (await directoryOf(io))
|
|
82
|
+
const target = resolve(io.cwd, directory)
|
|
83
|
+
const name = basename(target)
|
|
84
|
+
if (!named(name)) {
|
|
85
|
+
io.fail(
|
|
86
|
+
`${name} cannot name a package: use lowercase letters, digits, '-', '.' and '_'.`,
|
|
87
|
+
)
|
|
88
|
+
return 1
|
|
89
|
+
}
|
|
90
|
+
if (!empty(target)) {
|
|
91
|
+
io.fail(`${directory} is not an empty directory.`)
|
|
92
|
+
return 1
|
|
93
|
+
}
|
|
94
|
+
const manager = managerOf(io.userAgent)
|
|
95
|
+
write(target, name, manager)
|
|
96
|
+
const cd =
|
|
97
|
+
relative(io.cwd, target) === ''
|
|
98
|
+
? []
|
|
99
|
+
: [`cd ${/\s/u.test(directory) ? `"${directory}"` : directory}`]
|
|
100
|
+
const next = [...cd, ...steps[manager]].map((step) => ` ${step}`)
|
|
101
|
+
io.print(`\nCreated ${name}. Next:\n\n${next.join('\n')}\n`)
|
|
102
|
+
return 0
|
|
103
|
+
}
|
package/src/starter.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// The starter's files (D-130): the template as it is, its `gitignore` as
|
|
2
|
+
// `.gitignore`, and the package.json and README that name the application.
|
|
3
|
+
import {
|
|
4
|
+
cpSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
renameSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from 'node:fs'
|
|
10
|
+
import { join } from 'node:path'
|
|
11
|
+
import { fileURLToPath } from 'node:url'
|
|
12
|
+
|
|
13
|
+
export type Manager = 'npm' | 'yarn' | 'pnpm'
|
|
14
|
+
|
|
15
|
+
const template = fileURLToPath(new URL('../template', import.meta.url))
|
|
16
|
+
|
|
17
|
+
/** The packages the starter runs beside UltimaBase's, as the repository tests them. */
|
|
18
|
+
export const others = {
|
|
19
|
+
'@types/node': '^24.19.2',
|
|
20
|
+
graphql: '^17.0.2',
|
|
21
|
+
vite: '^8.3.4',
|
|
22
|
+
vitest: '^5.0.5',
|
|
23
|
+
} as const
|
|
24
|
+
|
|
25
|
+
/** `create-ultimabase`'s version, which the official packages share (D-60). */
|
|
26
|
+
export function ownVersion(): string {
|
|
27
|
+
const manifest: unknown = JSON.parse(
|
|
28
|
+
readFileSync(new URL('../package.json', import.meta.url), 'utf8'),
|
|
29
|
+
)
|
|
30
|
+
return String(Reflect.get(Object(manifest), 'version'))
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const manifestOf = (name: string): object => {
|
|
34
|
+
const ours = `^${ownVersion()}`
|
|
35
|
+
return {
|
|
36
|
+
name,
|
|
37
|
+
private: true,
|
|
38
|
+
type: 'module',
|
|
39
|
+
engines: { node: '>=24.7.0' },
|
|
40
|
+
scripts: {
|
|
41
|
+
dev: 'ultimabase dev',
|
|
42
|
+
start: 'ultimabase start',
|
|
43
|
+
test: 'vitest',
|
|
44
|
+
},
|
|
45
|
+
dependencies: {
|
|
46
|
+
'@ultimabase/graphql': ours,
|
|
47
|
+
'@ultimabase/http': ours,
|
|
48
|
+
graphql: others.graphql,
|
|
49
|
+
ultimabase: ours,
|
|
50
|
+
},
|
|
51
|
+
devDependencies: {
|
|
52
|
+
'@types/node': others['@types/node'],
|
|
53
|
+
'@ultimabase/testing': ours,
|
|
54
|
+
vite: others.vite,
|
|
55
|
+
vitest: others.vitest,
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** How `manager` runs a script other than `start` and `test`. */
|
|
61
|
+
const run = (manager: Manager): string =>
|
|
62
|
+
manager === 'npm' ? 'npm run' : manager
|
|
63
|
+
|
|
64
|
+
const readme = (name: string, manager: Manager): string => `# ${name}
|
|
65
|
+
|
|
66
|
+
An UltimaBase application. Node runs its TypeScript as it is written.
|
|
67
|
+
|
|
68
|
+
- \`${run(manager)} dev\` serves it on http://localhost:3000, GraphQL on
|
|
69
|
+
\`/graphql\`, and restarts it when a file changes; \`PORT\` sets another
|
|
70
|
+
port.
|
|
71
|
+
- \`${manager} start\` serves it in production.
|
|
72
|
+
- \`${manager} test\` runs its tests, each on a new in-memory database.
|
|
73
|
+
|
|
74
|
+
\`src/models.ts\` declares the models, \`src/app.ts\` the database and the
|
|
75
|
+
plugins, and \`ultimabase.config.ts\` names the application the
|
|
76
|
+
\`ultimabase\` command runs.
|
|
77
|
+
`
|
|
78
|
+
|
|
79
|
+
/** Writes the starter named `name` in `target`, an empty or missing directory. */
|
|
80
|
+
export function write(target: string, name: string, manager: Manager): void {
|
|
81
|
+
mkdirSync(target, { recursive: true })
|
|
82
|
+
cpSync(template, target, { recursive: true })
|
|
83
|
+
renameSync(join(target, 'gitignore'), join(target, '.gitignore'))
|
|
84
|
+
writeFileSync(
|
|
85
|
+
join(target, 'package.json'),
|
|
86
|
+
`${JSON.stringify(manifestOf(name), undefined, '\t')}\n`,
|
|
87
|
+
)
|
|
88
|
+
writeFileSync(join(target, 'README.md'), readme(name, manager))
|
|
89
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
node_modules
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Each test runs the application on a new in-memory database and a free port.
|
|
2
|
+
import { testApplication } from '@ultimabase/testing'
|
|
3
|
+
import { master } from 'ultimabase'
|
|
4
|
+
import { expect, it } from 'vitest'
|
|
5
|
+
import { app } from './app.ts'
|
|
6
|
+
import { Post } from './models.ts'
|
|
7
|
+
|
|
8
|
+
const running = testApplication(app)
|
|
9
|
+
|
|
10
|
+
it('lists the posts over GraphQL', async () => {
|
|
11
|
+
await Post.create({ data: { title: 'Hello' } }, { as: master })
|
|
12
|
+
const response = await fetch(`${running.url}/graphql`, {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
headers: { 'content-type': 'application/json' },
|
|
15
|
+
body: JSON.stringify({ query: '{ posts { edges { node { title } } } }' }),
|
|
16
|
+
})
|
|
17
|
+
expect(await response.json()).toEqual({
|
|
18
|
+
data: { posts: { edges: [{ node: { title: 'Hello' } }] } },
|
|
19
|
+
})
|
|
20
|
+
})
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// The application: its database, its plugins and its models.
|
|
2
|
+
import { graphqlPlugin } from '@ultimabase/graphql'
|
|
3
|
+
import { httpPlugin } from '@ultimabase/http'
|
|
4
|
+
import { defineApp, memory } from 'ultimabase'
|
|
5
|
+
import { Post } from './models.ts'
|
|
6
|
+
|
|
7
|
+
export const app = defineApp({
|
|
8
|
+
// For PostgreSQL: postgres({ url: process.env['DATABASE_URL'] }) from @ultimabase/postgres.
|
|
9
|
+
database: memory(),
|
|
10
|
+
plugins: [
|
|
11
|
+
httpPlugin(),
|
|
12
|
+
graphqlPlugin(),
|
|
13
|
+
// For REST too: restPlugin() from @ultimabase/rest.
|
|
14
|
+
],
|
|
15
|
+
models: [Post],
|
|
16
|
+
})
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// The application's models: each one a table, its fields and who may
|
|
2
|
+
// read and write it.
|
|
3
|
+
import { allow, defineModel } from 'ultimabase'
|
|
4
|
+
|
|
5
|
+
export const Post = defineModel({
|
|
6
|
+
name: 'Post',
|
|
7
|
+
fields: (f) => ({
|
|
8
|
+
title: f.string(),
|
|
9
|
+
}),
|
|
10
|
+
// Anyone may read and write posts; see `allow` for the other rules.
|
|
11
|
+
}).with({ access: allow.public })
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
// For the editor: Node runs the TypeScript as it is written.
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "ES2024",
|
|
5
|
+
"module": "NodeNext",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"exactOptionalPropertyTypes": true,
|
|
8
|
+
"noUncheckedIndexedAccess": true,
|
|
9
|
+
"verbatimModuleSyntax": true,
|
|
10
|
+
"erasableSyntaxOnly": true,
|
|
11
|
+
"allowImportingTsExtensions": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"types": ["node"]
|
|
15
|
+
},
|
|
16
|
+
"include": ["src", "ultimabase.config.ts"]
|
|
17
|
+
}
|