create-loutre 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.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCreateLoutre } from '../dist/cli.js'
4
+
5
+ process.exitCode = await runCreateLoutre(process.argv.slice(2))
package/dist/app.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import type { PackageManager, ProjectTarget } from './options.js';
2
+ export interface CreateProjectInput {
3
+ readonly cwd: string;
4
+ readonly directory: string;
5
+ readonly packageManager: PackageManager;
6
+ readonly target: ProjectTarget;
7
+ }
8
+ export interface CreateProjectResult {
9
+ readonly packageName: string;
10
+ readonly targetDirectory: string;
11
+ }
12
+ export declare class ProjectScaffolder {
13
+ create(input: CreateProjectInput): Promise<CreateProjectResult>;
14
+ }
15
+ export declare const createProject: import("@loutrejs/loutre").TaskDescriptor<CreateProjectInput, CreateProjectResult, string>;
16
+ declare const _default: import("@loutrejs/loutre").ApplicationDefinition<readonly [import("@loutrejs/loutre").ModuleInstance<{
17
+ readonly name: "CreateLoutreModule";
18
+ readonly providers: readonly [typeof ProjectScaffolder];
19
+ }>], undefined, readonly [import("@loutrejs/loutre").TaskDescriptor<CreateProjectInput, CreateProjectResult, string>], readonly []>;
20
+ export default _default;
package/dist/app.js ADDED
@@ -0,0 +1,62 @@
1
+ import { readdir } from 'node:fs/promises';
2
+ import { basename, resolve } from 'node:path';
3
+ import { defineApplication, defineModule, inject, task } from '@loutrejs/loutre';
4
+ import { writeStarter } from './starter.js';
5
+ export class ProjectScaffolder {
6
+ async create(input) {
7
+ const targetDirectory = resolve(input.cwd, input.directory);
8
+ const packageName = packageNameFor(targetDirectory);
9
+ await assertTargetIsEmpty(targetDirectory);
10
+ await writeStarter(targetDirectory, {
11
+ packageName,
12
+ packageManager: input.packageManager,
13
+ target: input.target,
14
+ });
15
+ return { packageName, targetDirectory };
16
+ }
17
+ }
18
+ export const createProject = task({
19
+ name: 'project.create',
20
+ factory: (scaffolder = inject(ProjectScaffolder)) => (input) => scaffolder.create(input),
21
+ });
22
+ const CreateLoutreModule = defineModule(() => ({
23
+ name: 'CreateLoutreModule',
24
+ providers: [ProjectScaffolder],
25
+ }));
26
+ export default defineApplication({
27
+ modules: [CreateLoutreModule()],
28
+ tasks: [createProject],
29
+ });
30
+ async function assertTargetIsEmpty(targetDirectory) {
31
+ try {
32
+ const entries = await readdir(targetDirectory);
33
+ if (entries.length > 0) {
34
+ throw new Error(`生成先が空ではありません: ${targetDirectory}`);
35
+ }
36
+ }
37
+ catch (error) {
38
+ if (isMissingDirectory(error))
39
+ return;
40
+ throw error;
41
+ }
42
+ }
43
+ function isMissingDirectory(error) {
44
+ return (error instanceof Error &&
45
+ 'code' in error &&
46
+ error.code === 'ENOENT');
47
+ }
48
+ function packageNameFor(targetDirectory) {
49
+ const name = basename(targetDirectory)
50
+ .trim()
51
+ .toLowerCase()
52
+ .replace(/\s+/gu, '-')
53
+ .replace(/[^a-z0-9._-]+/gu, '-')
54
+ .replace(/^-+|-+$/gu, '');
55
+ if (name.length === 0 ||
56
+ name.length > 214 ||
57
+ name.startsWith('.') ||
58
+ name.startsWith('_')) {
59
+ throw new Error(`npm package名を生成できません: ${targetDirectory}`);
60
+ }
61
+ return name;
62
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,16 @@
1
+ import { type PackageManager } from './options.js';
2
+ interface SelectOption {
3
+ readonly value: string;
4
+ readonly label: string;
5
+ }
6
+ export interface CreateLoutreCliIO {
7
+ readonly cwd: string;
8
+ readonly detectedPackageManager?: PackageManager;
9
+ readonly prompt?: (message: string, initialValue: string) => Promise<string | undefined>;
10
+ readonly select?: (message: string, options: readonly SelectOption[], initialValue: string) => Promise<string | undefined>;
11
+ readonly install: (directory: string, packageManager: PackageManager) => Promise<number>;
12
+ readonly stdout: (value: string) => void;
13
+ readonly stderr: (value: string) => void;
14
+ }
15
+ export declare function runCreateLoutre(args: readonly string[], io?: CreateLoutreCliIO): Promise<number>;
16
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,208 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { relative } from 'node:path';
3
+ import { isCancel, select, text } from '@clack/prompts';
4
+ import { Command, CommanderError, Option } from 'commander';
5
+ import { bootstrap } from '@loutrejs/loutre/host';
6
+ import application, { createProject } from './app.js';
7
+ import { installCommand, packageManagerLabels, packageManagers, projectTargets, runScriptCommand, targetLabels, } from './options.js';
8
+ export async function runCreateLoutre(args, io = createProcessIO()) {
9
+ const parsed = parseArgs(args);
10
+ if ('error' in parsed) {
11
+ io.stderr(parsed.error);
12
+ io.stderr(helpText());
13
+ return 2;
14
+ }
15
+ if (parsed.help) {
16
+ io.stdout(helpText());
17
+ return 0;
18
+ }
19
+ const directory = await resolveDirectory(parsed.directory, parsed.yes, io);
20
+ if (!directory)
21
+ return 2;
22
+ const target = await resolveTarget(parsed.target, parsed.yes, io);
23
+ if (!target)
24
+ return 2;
25
+ const packageManager = await resolvePackageManager(parsed.packageManager, parsed.yes, io);
26
+ if (!packageManager)
27
+ return 2;
28
+ const app = bootstrap({ application });
29
+ try {
30
+ const result = await app.run(createProject, {
31
+ cwd: io.cwd,
32
+ directory,
33
+ packageManager,
34
+ target,
35
+ });
36
+ io.stdout(`Loutre Applicationを作成しました: ${result.targetDirectory}`);
37
+ io.stdout(`Target: ${targetLabels[target]}`);
38
+ io.stdout(`Package manager: ${packageManagerLabels[packageManager]}`);
39
+ if (parsed.install) {
40
+ io.stdout('依存関係をインストールします。');
41
+ const installCode = await io.install(result.targetDirectory, packageManager);
42
+ if (installCode !== 0) {
43
+ io.stderr(`${installCommand(packageManager)}に失敗しました。生成したファイルは残しています。`);
44
+ return installCode;
45
+ }
46
+ }
47
+ const nextDirectory = relative(io.cwd, result.targetDirectory);
48
+ io.stdout('');
49
+ io.stdout('次のコマンド:');
50
+ if (nextDirectory && nextDirectory !== '.') {
51
+ io.stdout(` cd ${quoteShellArgument(nextDirectory)}`);
52
+ }
53
+ if (!parsed.install)
54
+ io.stdout(` ${installCommand(packageManager)}`);
55
+ io.stdout(` ${nextCommand(packageManager, target)}`);
56
+ return 0;
57
+ }
58
+ catch (error) {
59
+ io.stderr(error instanceof Error ? error.message : String(error));
60
+ return 1;
61
+ }
62
+ finally {
63
+ await app.close('create-loutre-complete');
64
+ }
65
+ }
66
+ function parseArgs(args) {
67
+ const command = new Command()
68
+ .name('create-loutre')
69
+ .helpOption(false)
70
+ .exitOverride()
71
+ .configureOutput({
72
+ writeOut: () => undefined,
73
+ writeErr: () => undefined,
74
+ })
75
+ .argument('[directory]')
76
+ .addOption(new Option('--target <target>').choices([...projectTargets]))
77
+ .addOption(new Option('--package-manager <package-manager>').choices([
78
+ ...packageManagers,
79
+ ]))
80
+ .option('-y, --yes', '未指定の選択肢に既定値を使用する')
81
+ .option('--no-install', '依存関係をinstallしない')
82
+ .option('-h, --help', 'helpを表示する')
83
+ .allowExcessArguments(false);
84
+ try {
85
+ command.parse(args.filter((value) => value !== '--'), { from: 'user' });
86
+ }
87
+ catch (error) {
88
+ if (error instanceof CommanderError)
89
+ return { error: error.message };
90
+ throw error;
91
+ }
92
+ const options = command.opts();
93
+ const directory = command.processedArgs[0];
94
+ return {
95
+ ...(directory === undefined ? {} : { directory }),
96
+ ...(options.target === undefined ? {} : { target: options.target }),
97
+ ...(options.packageManager === undefined
98
+ ? {}
99
+ : { packageManager: options.packageManager }),
100
+ help: options.help ?? false,
101
+ yes: options.yes ?? false,
102
+ install: options.install,
103
+ };
104
+ }
105
+ async function resolveDirectory(requested, yes, io) {
106
+ if (requested)
107
+ return requested;
108
+ if (yes)
109
+ return 'loutre-app';
110
+ if (!io.prompt) {
111
+ io.stderr('生成先を指定してください。例: npm create loutre@latest my-app');
112
+ return undefined;
113
+ }
114
+ const answer = await io.prompt('Project name', 'loutre-app');
115
+ if (answer === undefined)
116
+ return undefined;
117
+ return answer.trim() || 'loutre-app';
118
+ }
119
+ async function resolveTarget(requested, yes, io) {
120
+ if (requested)
121
+ return requested;
122
+ if (yes || !io.select)
123
+ return 'node';
124
+ const selected = await io.select('Target', projectTargets.map((value) => ({ value, label: targetLabels[value] })), 'node');
125
+ return projectTargets.find((target) => target === selected);
126
+ }
127
+ async function resolvePackageManager(requested, yes, io) {
128
+ if (requested)
129
+ return requested;
130
+ const initialValue = io.detectedPackageManager ?? 'npm';
131
+ if (yes || !io.select)
132
+ return initialValue;
133
+ const selected = await io.select('Package manager', packageManagers.map((value) => ({
134
+ value,
135
+ label: packageManagerLabels[value],
136
+ })), initialValue);
137
+ return packageManagers.find((packageManager) => packageManager === selected);
138
+ }
139
+ function createProcessIO() {
140
+ const interactive = process.stdin.isTTY && process.stdout.isTTY;
141
+ return {
142
+ cwd: process.cwd(),
143
+ detectedPackageManager: detectPackageManager(process.env.npm_config_user_agent, globalThis),
144
+ ...(interactive ? { prompt: terminalPrompt, select: terminalSelect } : {}),
145
+ install: installDependencies,
146
+ stdout: (value) => process.stdout.write(`${value}\n`),
147
+ stderr: (value) => process.stderr.write(`${value}\n`),
148
+ };
149
+ }
150
+ async function terminalPrompt(message, initialValue) {
151
+ const result = await text({ message, defaultValue: initialValue });
152
+ return isCancel(result) ? undefined : result;
153
+ }
154
+ async function terminalSelect(message, options, initialValue) {
155
+ const result = await select({ message, options: [...options], initialValue });
156
+ return isCancel(result) ? undefined : result;
157
+ }
158
+ function installDependencies(directory, packageManager) {
159
+ const command = executableFor(packageManager);
160
+ return new Promise((resolvePromise, reject) => {
161
+ const child = spawn(command, ['install'], {
162
+ cwd: directory,
163
+ stdio: 'inherit',
164
+ });
165
+ child.once('error', reject);
166
+ child.once('exit', (code, signal) => {
167
+ resolvePromise(code ?? (signal ? 1 : 0));
168
+ });
169
+ });
170
+ }
171
+ function executableFor(packageManager) {
172
+ if (process.platform !== 'win32')
173
+ return packageManager;
174
+ return packageManager === 'npm' ||
175
+ packageManager === 'pnpm' ||
176
+ packageManager === 'yarn'
177
+ ? `${packageManager}.cmd`
178
+ : packageManager;
179
+ }
180
+ function detectPackageManager(userAgent, runtime) {
181
+ const globals = runtime;
182
+ if (globals.Bun !== undefined)
183
+ return 'bun';
184
+ if (globals.Deno !== undefined)
185
+ return 'deno';
186
+ const name = userAgent?.split('/')[0];
187
+ return (packageManagers.find((packageManager) => packageManager === name) ?? 'npm');
188
+ }
189
+ function nextCommand(packageManager, target) {
190
+ return runScriptCommand(packageManager, target === 'aws-lambda' ? 'build' : 'dev');
191
+ }
192
+ function quoteShellArgument(value) {
193
+ return /^[A-Za-z0-9_./-]+$/u.test(value)
194
+ ? value
195
+ : `'${value.replaceAll("'", "'\\''")}'`;
196
+ }
197
+ function helpText() {
198
+ return [
199
+ 'Usage: create-loutre [directory] [options]',
200
+ '',
201
+ 'Options:',
202
+ ' --target <target> node | bun | deno | cloudflare-workers | aws-lambda',
203
+ ' --package-manager <package-manager> npm | pnpm | yarn | bun | deno',
204
+ ' -y, --yes 未指定の選択肢に既定値を使用する',
205
+ ' --no-install 依存関係をinstallしない',
206
+ ' -h, --help helpを表示する',
207
+ ].join('\n');
208
+ }
@@ -0,0 +1,3 @@
1
+ export { createProject, ProjectScaffolder, type CreateProjectInput, type CreateProjectResult, } from './app.js';
2
+ export { runCreateLoutre, type CreateLoutreCliIO } from './cli.js';
3
+ export { packageManagers, projectTargets, type PackageManager, type ProjectTarget, } from './options.js';
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createProject, ProjectScaffolder, } from './app.js';
2
+ export { runCreateLoutre } from './cli.js';
3
+ export { packageManagers, projectTargets, } from './options.js';
@@ -0,0 +1,8 @@
1
+ export declare const projectTargets: readonly ['node', 'bun', 'deno', 'cloudflare-workers', 'aws-lambda'];
2
+ export type ProjectTarget = (typeof projectTargets)[number];
3
+ export declare const packageManagers: readonly ['npm', 'pnpm', 'yarn', 'bun', 'deno'];
4
+ export type PackageManager = (typeof packageManagers)[number];
5
+ export declare const targetLabels: Readonly<Record<ProjectTarget, string>>;
6
+ export declare const packageManagerLabels: Readonly<Record<PackageManager, string>>;
7
+ export declare function installCommand(packageManager: PackageManager): string;
8
+ export declare function runScriptCommand(packageManager: PackageManager, script: string): string;
@@ -0,0 +1,39 @@
1
+ export const projectTargets = [
2
+ 'node',
3
+ 'bun',
4
+ 'deno',
5
+ 'cloudflare-workers',
6
+ 'aws-lambda',
7
+ ];
8
+ export const packageManagers = ['npm', 'pnpm', 'yarn', 'bun', 'deno'];
9
+ export const targetLabels = {
10
+ node: 'Node.js',
11
+ bun: 'Bun',
12
+ deno: 'Deno',
13
+ 'cloudflare-workers': 'Cloudflare Workers',
14
+ 'aws-lambda': 'AWS Lambda',
15
+ };
16
+ export const packageManagerLabels = {
17
+ npm: 'npm',
18
+ pnpm: 'pnpm',
19
+ yarn: 'Yarn',
20
+ bun: 'Bun',
21
+ deno: 'Deno',
22
+ };
23
+ export function installCommand(packageManager) {
24
+ return `${packageManager} install`;
25
+ }
26
+ export function runScriptCommand(packageManager, script) {
27
+ switch (packageManager) {
28
+ case 'npm':
29
+ return `npm run ${script}`;
30
+ case 'pnpm':
31
+ return `pnpm run ${script}`;
32
+ case 'yarn':
33
+ return `yarn run ${script}`;
34
+ case 'bun':
35
+ return `bun run ${script}`;
36
+ case 'deno':
37
+ return `deno task ${script}`;
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ import { type PackageManager, type ProjectTarget } from './options.js';
2
+ export interface StarterOptions {
3
+ readonly packageName: string;
4
+ readonly packageManager: PackageManager;
5
+ readonly target: ProjectTarget;
6
+ }
7
+ export declare function writeStarter(targetDirectory: string, options: StarterOptions): Promise<void>;
@@ -0,0 +1,208 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import { cp, readFile, rename, writeFile } from 'node:fs/promises';
3
+ import { join } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { runScriptCommand, targetLabels, } from './options.js';
6
+ const packageManifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
7
+ const loutreVersion = requiredDependencyVersion('@loutrejs/loutre');
8
+ function requiredDependencyVersion(name) {
9
+ const version = packageManifest.dependencies[name];
10
+ if (!version)
11
+ throw new Error(`${name}のversionがpackage.jsonにありません。`);
12
+ return version;
13
+ }
14
+ const baseTemplateDirectory = fileURLToPath(new URL('../templates/base/', import.meta.url));
15
+ const targetsTemplateDirectory = fileURLToPath(new URL('../templates/targets/', import.meta.url));
16
+ const renamedTemplateFiles = new Map([
17
+ ['_gitignore', '.gitignore'],
18
+ ['_oxlintrc.json', '.oxlintrc.json'],
19
+ ['_oxfmtrc.json', '.oxfmtrc.json'],
20
+ ]);
21
+ export async function writeStarter(targetDirectory, options) {
22
+ await cp(baseTemplateDirectory, targetDirectory, { recursive: true });
23
+ await cp(join(targetsTemplateDirectory, options.target), targetDirectory, {
24
+ recursive: true,
25
+ });
26
+ for (const [source, destination] of renamedTemplateFiles) {
27
+ await rename(join(targetDirectory, source), join(targetDirectory, destination));
28
+ }
29
+ await writeFile(join(targetDirectory, 'package.json'), renderPackageJson(options), 'utf8');
30
+ await replaceText(join(targetDirectory, 'tsconfig.json'), '"__LOUTRE_TYPES__"', JSON.stringify(targetManifest(options.target).types));
31
+ await renderTextTemplate(join(targetDirectory, 'README.md'), {
32
+ targetLabel: targetLabels[options.target],
33
+ developmentSection: developmentSection(options),
34
+ verifyCommand: runScriptCommand(options.packageManager, 'verify'),
35
+ deploymentSection: deploymentSection(options),
36
+ });
37
+ if (options.target === 'cloudflare-workers') {
38
+ await renderTextTemplate(join(targetDirectory, 'wrangler.jsonc'), {
39
+ packageName: workerNameFor(options.packageName),
40
+ compatibilityDate: new Date().toISOString().slice(0, 10),
41
+ });
42
+ }
43
+ }
44
+ function targetManifest(target) {
45
+ switch (target) {
46
+ case 'node':
47
+ return {
48
+ scripts: {
49
+ dev: 'tsx watch src/main.ts',
50
+ build: 'tsc -p tsconfig.build.json',
51
+ start: 'node dist/main.js',
52
+ },
53
+ dependencies: { '@loutrejs/node': loutreVersion },
54
+ devDependencies: { '@types/node': '^22.20.1', tsx: '^4.23.12' },
55
+ engines: { node: '>=22' },
56
+ types: ['node'],
57
+ };
58
+ case 'bun':
59
+ return {
60
+ scripts: {
61
+ dev: 'bun --watch src/main.ts',
62
+ build: 'bun build src/main.ts --outdir dist --target bun',
63
+ start: 'bun dist/main.js',
64
+ },
65
+ devDependencies: { '@types/bun': '^1.4.0' },
66
+ types: ['bun'],
67
+ };
68
+ case 'deno':
69
+ return {
70
+ scripts: {
71
+ dev: 'deno run -A --watch src/main.ts',
72
+ start: 'deno run -A src/main.ts',
73
+ },
74
+ typecheck: 'tsc --noEmit --allowImportingTsExtensions',
75
+ types: [],
76
+ };
77
+ case 'cloudflare-workers':
78
+ return {
79
+ scripts: {
80
+ dev: 'wrangler dev',
81
+ build: 'wrangler deploy --dry-run --outdir dist',
82
+ deploy: 'wrangler deploy',
83
+ },
84
+ devDependencies: { wrangler: '^4.127.1' },
85
+ types: [],
86
+ };
87
+ case 'aws-lambda':
88
+ return {
89
+ scripts: {
90
+ build: 'esbuild src/main.ts --bundle --platform=node --target=node22 --format=esm --outfile=dist/index.mjs',
91
+ },
92
+ devDependencies: { '@types/node': '^22.20.1', esbuild: '^0.28.2' },
93
+ engines: { node: '>=22' },
94
+ types: ['node'],
95
+ };
96
+ }
97
+ }
98
+ function renderPackageJson(options) {
99
+ const target = targetManifest(options.target);
100
+ const build = target.scripts.build;
101
+ const typecheck = target.typecheck ?? 'tsc --noEmit';
102
+ const verifyBuild = build === undefined ? '' : ` && ${build}`;
103
+ return `${JSON.stringify({
104
+ name: options.packageName,
105
+ version: '0.1.0',
106
+ private: true,
107
+ type: 'module',
108
+ ...(target.engines === undefined ? {} : { engines: target.engines }),
109
+ scripts: {
110
+ ...target.scripts,
111
+ typecheck,
112
+ check: `${typecheck} && loutre check --entry src/app.ts`,
113
+ test: 'vitest run',
114
+ 'test:watch': 'vitest',
115
+ lint: 'oxlint',
116
+ 'lint:fix': 'oxlint --fix',
117
+ format: 'oxfmt',
118
+ 'format:check': 'oxfmt --check',
119
+ verify: `oxfmt --check && oxlint && ${typecheck} && loutre check --entry src/app.ts && vitest run${verifyBuild}`,
120
+ },
121
+ dependencies: {
122
+ '@loutrejs/loutre': loutreVersion,
123
+ zod: '^4.4.3',
124
+ ...target.dependencies,
125
+ },
126
+ devDependencies: {
127
+ '@loutrejs/cli': loutreVersion,
128
+ oxfmt: '^0.65.0',
129
+ oxlint: '^1.80.0',
130
+ typescript: '^7.0.2',
131
+ vitest: '^4.1.11',
132
+ ...target.devDependencies,
133
+ },
134
+ }, null, 2)}\n`;
135
+ }
136
+ function developmentSection(options) {
137
+ if (options.target === 'aws-lambda') {
138
+ return [
139
+ '## 開発',
140
+ '',
141
+ 'Lambda targetはlocal HTTP listenerを起動しません。Applicationの挙動はtestで確認します。',
142
+ '',
143
+ '```sh',
144
+ runScriptCommand(options.packageManager, 'test:watch'),
145
+ '```',
146
+ ].join('\n');
147
+ }
148
+ return [
149
+ '## 開発',
150
+ '',
151
+ '```sh',
152
+ runScriptCommand(options.packageManager, 'dev'),
153
+ '```',
154
+ '',
155
+ options.target === 'cloudflare-workers'
156
+ ? 'WranglerがlocalのCloudflare Workers環境を起動します。'
157
+ : '<http://127.0.0.1:3000> へアクセスするとJSONレスポンスを返します。',
158
+ ].join('\n');
159
+ }
160
+ function deploymentSection(options) {
161
+ if (options.target === 'cloudflare-workers') {
162
+ return [
163
+ '## Deploy',
164
+ '',
165
+ '```sh',
166
+ runScriptCommand(options.packageManager, 'deploy'),
167
+ '```',
168
+ ].join('\n');
169
+ }
170
+ if (options.target === 'aws-lambda') {
171
+ return [
172
+ '## Build',
173
+ '',
174
+ '```sh',
175
+ runScriptCommand(options.packageManager, 'build'),
176
+ '```',
177
+ '',
178
+ '`dist/index.mjs`の`handler`をAWS Lambdaのhandlerとしてdeployします。',
179
+ ].join('\n');
180
+ }
181
+ return [
182
+ '## Production',
183
+ '',
184
+ '```sh',
185
+ ...(options.target === 'deno'
186
+ ? []
187
+ : [runScriptCommand(options.packageManager, 'build')]),
188
+ runScriptCommand(options.packageManager, 'start'),
189
+ '```',
190
+ ].join('\n');
191
+ }
192
+ async function renderTextTemplate(path, values) {
193
+ let content = await readFile(path, 'utf8');
194
+ for (const [name, value] of Object.entries(values)) {
195
+ content = content.replaceAll(`{{${name}}}`, value);
196
+ }
197
+ await writeFile(path, content, 'utf8');
198
+ }
199
+ async function replaceText(path, search, replacement) {
200
+ const content = await readFile(path, 'utf8');
201
+ await writeFile(path, content.replaceAll(search, replacement), 'utf8');
202
+ }
203
+ function workerNameFor(packageName) {
204
+ return (packageName
205
+ .replace(/[^a-z0-9-]+/gu, '-')
206
+ .replace(/^-+|-+$/gu, '')
207
+ .slice(0, 63) || 'loutre-worker');
208
+ }
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "create-loutre",
3
+ "version": "0.1.0",
4
+ "description": "Loutre project initializer",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "create-loutre": "bin/create-loutre.js"
9
+ },
10
+ "files": [
11
+ "bin",
12
+ "dist",
13
+ "templates"
14
+ ],
15
+ "publishConfig": {
16
+ "access": "public",
17
+ "provenance": true
18
+ },
19
+ "dependencies": {
20
+ "@clack/prompts": "^1.7.0",
21
+ "@loutrejs/loutre": "^0.1.0",
22
+ "commander": "^15.0.0"
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "https://github.com/come25136/loutrejs",
27
+ "directory": "packages/create-loutre"
28
+ }
29
+ }
@@ -0,0 +1,13 @@
1
+ # Loutre Application
2
+
3
+ Loutreで作成した{{targetLabel}}向けApplicationです。
4
+
5
+ {{developmentSection}}
6
+
7
+ ## 検証
8
+
9
+ ```sh
10
+ {{verifyCommand}}
11
+ ```
12
+
13
+ {{deploymentSection}}
@@ -0,0 +1,4 @@
1
+ node_modules/
2
+ dist/
3
+ coverage/
4
+ .env
@@ -0,0 +1,13 @@
1
+ {
2
+ "$schema": "./node_modules/oxfmt/configuration_schema.json",
3
+ "printWidth": 80,
4
+ "tabWidth": 2,
5
+ "useTabs": false,
6
+ "semi": false,
7
+ "singleQuote": true,
8
+ "trailingComma": "all",
9
+ "endOfLine": "lf",
10
+ "proseWrap": "preserve",
11
+ "sortPackageJson": false,
12
+ "ignorePatterns": ["dist/**", "coverage/**", "node_modules/**"]
13
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "categories": {
4
+ "correctness": "error",
5
+ "suspicious": "error"
6
+ },
7
+ "ignorePatterns": ["dist/**", "coverage/**", "node_modules/**"]
8
+ }
@@ -0,0 +1,15 @@
1
+ import { bootstrap } from '@loutrejs/loutre/host'
2
+ import { expect, it } from 'vitest'
3
+ import application from './app.js'
4
+
5
+ it('GET / がLoutre Applicationのレスポンスを返す', async () => {
6
+ const app = bootstrap({ application })
7
+ try {
8
+ const response = await app.fetch(new Request('http://localhost/'))
9
+
10
+ expect(response.status).toBe(200)
11
+ expect(await response.json()).toEqual({ message: 'Hello from Loutre!' })
12
+ } finally {
13
+ await app.close('test-complete')
14
+ }
15
+ })
@@ -0,0 +1,55 @@
1
+ import {
2
+ contract,
3
+ defineApplication,
4
+ defineModule,
5
+ implementation,
6
+ procedure,
7
+ } from '@loutrejs/loutre'
8
+ import { http } from '@loutrejs/loutre/http'
9
+ import { z } from 'zod'
10
+
11
+ const AppContract = contract(
12
+ {
13
+ hello: procedure({
14
+ protocols: {
15
+ http: http({
16
+ method: 'GET',
17
+ path: '/',
18
+ responses: {
19
+ ok: {
20
+ status: 200,
21
+ body: z.object({
22
+ message: z.string(),
23
+ }),
24
+ },
25
+ },
26
+ pipeline: [http.controller],
27
+ }),
28
+ },
29
+ }),
30
+ },
31
+ { name: 'AppContract' },
32
+ )
33
+
34
+ const AppController = implementation({
35
+ name: 'AppController',
36
+ contract: AppContract,
37
+ protocol: http,
38
+ factory: () => ({
39
+ async hello(ctx) {
40
+ return ctx.response.ok({
41
+ body: { message: 'Hello from Loutre!' },
42
+ })
43
+ },
44
+ }),
45
+ })
46
+
47
+ const AppModule = defineModule(() => ({
48
+ name: 'AppModule',
49
+ description: 'HTTP Applicationのentry module',
50
+ implementations: [AppController],
51
+ }))
52
+
53
+ export default defineApplication({
54
+ modules: [AppModule()],
55
+ })
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2024",
4
+ "module": "NodeNext",
5
+ "moduleResolution": "NodeNext",
6
+ "strict": true,
7
+ "noUncheckedIndexedAccess": true,
8
+ "exactOptionalPropertyTypes": true,
9
+ "useDefineForClassFields": true,
10
+ "rootDir": "src",
11
+ "outDir": "dist",
12
+ "skipLibCheck": true,
13
+ "types": "__LOUTRE_TYPES__"
14
+ },
15
+ "include": ["src/**/*.ts"]
16
+ }
@@ -0,0 +1,7 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ['src/**/*.test.ts'],
6
+ },
7
+ })
@@ -0,0 +1,4 @@
1
+ import { awsLambdaRuntime } from '@loutrejs/loutre/runtime/aws-lambda'
2
+ import application from './app.js'
3
+
4
+ export const handler = awsLambdaRuntime.bind({ application })
@@ -0,0 +1,10 @@
1
+ import { bunRuntime } from '@loutrejs/loutre/runtime/bun'
2
+ import application from './app.js'
3
+
4
+ await bunRuntime.serve({
5
+ application,
6
+ hostname: '127.0.0.1',
7
+ port: 3000,
8
+ })
9
+
10
+ console.log('Loutre is swimming at http://127.0.0.1:3000')
@@ -0,0 +1,4 @@
1
+ import { cloudflareWorkersRuntime } from '@loutrejs/loutre/runtime/cloudflare-workers'
2
+ import application from './app.js'
3
+
4
+ export default cloudflareWorkersRuntime.bind({ application })
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "./node_modules/wrangler/config-schema.json",
3
+ "name": "{{packageName}}",
4
+ "main": "src/main.ts",
5
+ "compatibility_date": "{{compatibilityDate}}",
6
+ }
@@ -0,0 +1,10 @@
1
+ import { denoRuntime } from '@loutrejs/loutre/runtime/deno'
2
+ import application from './app.ts'
3
+
4
+ await denoRuntime.serve({
5
+ application,
6
+ hostname: '127.0.0.1',
7
+ port: 3000,
8
+ })
9
+
10
+ console.log('Loutre is swimming at http://127.0.0.1:3000')
@@ -0,0 +1,16 @@
1
+ import { nodeRuntime } from '@loutrejs/node'
2
+ import application from './app.js'
3
+
4
+ const server = await nodeRuntime.serve({
5
+ application,
6
+ hostname: '127.0.0.1',
7
+ port: 3000,
8
+ })
9
+
10
+ console.log('Loutre is swimming at http://127.0.0.1:3000')
11
+
12
+ for (const signal of ['SIGINT', 'SIGTERM'] as const) {
13
+ process.once(signal, () => {
14
+ void server.close(signal)
15
+ })
16
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "exclude": ["src/**/*.test.ts"]
4
+ }