create-loutre 0.5.0 → 0.6.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/dist/app.d.ts +3 -2
- package/dist/app.js +4 -5
- package/dist/cli.js +6 -5
- package/dist/options.d.ts +4 -2
- package/dist/options.js +3 -0
- package/dist/starter.js +28 -8
- package/package.json +2 -2
- package/templates/base/README.md +14 -0
- package/templates/base/src/app.test.ts +7 -3
- package/templates/base/src/app.ts +3 -48
- package/templates/base/src/hello/contract.ts +17 -0
- package/templates/base/src/hello/controller.ts +14 -0
- package/templates/base/tsconfig.json +9 -0
package/dist/app.d.ts
CHANGED
|
@@ -12,9 +12,10 @@ export interface CreateProjectResult {
|
|
|
12
12
|
export declare class ProjectScaffolder {
|
|
13
13
|
create(input: CreateProjectInput): Promise<CreateProjectResult>;
|
|
14
14
|
}
|
|
15
|
-
export declare const createProject: import("@loutrejs/loutre").
|
|
15
|
+
export declare const createProject: import("@loutrejs/loutre/tasks").TaskDefinition<CreateProjectInput, CreateProjectResult>;
|
|
16
16
|
declare const _default: import("@loutrejs/loutre").ApplicationDefinition<readonly [import("@loutrejs/loutre").ModuleInstance<{
|
|
17
17
|
readonly name: "CreateLoutreModule";
|
|
18
18
|
readonly providers: readonly [typeof ProjectScaffolder];
|
|
19
|
-
|
|
19
|
+
readonly executions: readonly [import("@loutrejs/loutre/tasks").TaskDefinition<CreateProjectInput, CreateProjectResult>];
|
|
20
|
+
}>], undefined>;
|
|
20
21
|
export default _default;
|
package/dist/app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
2
|
import { basename, resolve } from 'node:path';
|
|
3
|
-
import { defineApplication, defineModule, inject
|
|
3
|
+
import { defineApplication, defineModule, inject } from '@loutrejs/loutre';
|
|
4
|
+
import { task } from '@loutrejs/loutre/tasks';
|
|
4
5
|
import { writeStarter } from './starter.js';
|
|
5
6
|
export class ProjectScaffolder {
|
|
6
7
|
async create(input) {
|
|
@@ -22,11 +23,9 @@ export const createProject = task({
|
|
|
22
23
|
const CreateLoutreModule = defineModule(() => ({
|
|
23
24
|
name: 'CreateLoutreModule',
|
|
24
25
|
providers: [ProjectScaffolder],
|
|
26
|
+
executions: [createProject],
|
|
25
27
|
}));
|
|
26
|
-
export default defineApplication({
|
|
27
|
-
modules: [CreateLoutreModule()],
|
|
28
|
-
tasks: [createProject],
|
|
29
|
-
});
|
|
28
|
+
export default defineApplication({ modules: [CreateLoutreModule()] });
|
|
30
29
|
async function assertTargetIsEmpty(targetDirectory) {
|
|
31
30
|
try {
|
|
32
31
|
const entries = await readdir(targetDirectory);
|
package/dist/cli.js
CHANGED
|
@@ -2,10 +2,10 @@ import { spawn } from 'node:child_process';
|
|
|
2
2
|
import { relative } from 'node:path';
|
|
3
3
|
import { isCancel, select, text } from '@clack/prompts';
|
|
4
4
|
import { Command, CommanderError, Option } from 'commander';
|
|
5
|
-
import {
|
|
5
|
+
import { bootstrapApplication } from '@loutrejs/loutre';
|
|
6
6
|
import { detectPresentationTerminal, renderLoutreBrand, } from '@loutrejs/loutre/presentation';
|
|
7
7
|
import application, { createProject } from './app.js';
|
|
8
|
-
import { installCommand, packageManagerLabels, packageManagers, projectTargets, runScriptCommand, targetLabels, } from './options.js';
|
|
8
|
+
import { createIsTarget, installCommand, packageManagerLabels, packageManagers, projectTargets, runScriptCommand, targetLabels, } from './options.js';
|
|
9
9
|
export async function runCreateLoutre(args, io = createProcessIO()) {
|
|
10
10
|
const parsed = parseArgs(args);
|
|
11
11
|
if ('error' in parsed) {
|
|
@@ -27,9 +27,9 @@ export async function runCreateLoutre(args, io = createProcessIO()) {
|
|
|
27
27
|
const packageManager = await resolvePackageManager(parsed.packageManager, parsed.yes, io);
|
|
28
28
|
if (!packageManager)
|
|
29
29
|
return 2;
|
|
30
|
-
const app =
|
|
30
|
+
const app = await bootstrapApplication({ application });
|
|
31
31
|
try {
|
|
32
|
-
const result = await app.run(createProject, {
|
|
32
|
+
const result = await app.tasks.run(createProject, {
|
|
33
33
|
cwd: io.cwd,
|
|
34
34
|
directory,
|
|
35
35
|
packageManager,
|
|
@@ -190,7 +190,8 @@ function detectPackageManager(userAgent, runtime) {
|
|
|
190
190
|
return (packageManagers.find((packageManager) => packageManager === name) ?? 'npm');
|
|
191
191
|
}
|
|
192
192
|
function nextCommand(packageManager, target) {
|
|
193
|
-
|
|
193
|
+
const isTarget = createIsTarget(target);
|
|
194
|
+
return runScriptCommand(packageManager, isTarget('aws-lambda') ? 'build' : 'dev');
|
|
194
195
|
}
|
|
195
196
|
function quoteShellArgument(value) {
|
|
196
197
|
return /^[A-Za-z0-9_./-]+$/u.test(value)
|
package/dist/options.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
export
|
|
1
|
+
import type { RuntimeEngine } from '@loutrejs/loutre/runtime';
|
|
2
|
+
export declare const projectTargets: readonly ["node", "bun", "deno", "cloudflare-workers", "aws-lambda"];
|
|
3
|
+
export type ProjectTarget = Exclude<RuntimeEngine, 'electron' | 'unknown'>;
|
|
4
|
+
export declare function createIsTarget(target: ProjectTarget): (candidate: ProjectTarget) => boolean;
|
|
3
5
|
export declare const packageManagers: readonly ['npm', 'pnpm', 'yarn', 'bun', 'deno'];
|
|
4
6
|
export type PackageManager = (typeof packageManagers)[number];
|
|
5
7
|
export declare const targetLabels: Readonly<Record<ProjectTarget, string>>;
|
package/dist/options.js
CHANGED
|
@@ -5,6 +5,9 @@ export const projectTargets = [
|
|
|
5
5
|
'cloudflare-workers',
|
|
6
6
|
'aws-lambda',
|
|
7
7
|
];
|
|
8
|
+
export function createIsTarget(target) {
|
|
9
|
+
return (candidate) => target === candidate;
|
|
10
|
+
}
|
|
8
11
|
export const packageManagers = ['npm', 'pnpm', 'yarn', 'bun', 'deno'];
|
|
9
12
|
export const targetLabels = {
|
|
10
13
|
node: 'Node.js',
|
package/dist/starter.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { readFileSync } from 'node:fs';
|
|
2
|
-
import { cp, readFile, rename, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { cp, readFile, readdir, rename, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
|
-
import { runScriptCommand, targetLabels, } from './options.js';
|
|
5
|
+
import { createIsTarget, runScriptCommand, targetLabels, } from './options.js';
|
|
6
6
|
const packageManifest = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
7
7
|
const loutreVersion = requiredDependencyVersion('@loutrejs/loutre');
|
|
8
8
|
function requiredDependencyVersion(name) {
|
|
@@ -19,6 +19,7 @@ const renamedTemplateFiles = new Map([
|
|
|
19
19
|
['_oxfmtrc.json', '.oxfmtrc.json'],
|
|
20
20
|
]);
|
|
21
21
|
export async function writeStarter(targetDirectory, options) {
|
|
22
|
+
const isTarget = createIsTarget(options.target);
|
|
22
23
|
await cp(baseTemplateDirectory, targetDirectory, { recursive: true });
|
|
23
24
|
await cp(join(targetsTemplateDirectory, options.target), targetDirectory, {
|
|
24
25
|
recursive: true,
|
|
@@ -34,7 +35,10 @@ export async function writeStarter(targetDirectory, options) {
|
|
|
34
35
|
verifyCommand: runScriptCommand(options.packageManager, 'verify'),
|
|
35
36
|
deploymentSection: deploymentSection(options),
|
|
36
37
|
});
|
|
37
|
-
if (
|
|
38
|
+
if (isTarget('deno')) {
|
|
39
|
+
await rewriteLocalImportExtensionsForDeno(join(targetDirectory, 'src'));
|
|
40
|
+
}
|
|
41
|
+
if (isTarget('cloudflare-workers')) {
|
|
38
42
|
await renderTextTemplate(join(targetDirectory, 'wrangler.jsonc'), {
|
|
39
43
|
packageName: workerNameFor(options.packageName),
|
|
40
44
|
compatibilityDate: new Date().toISOString().slice(0, 10),
|
|
@@ -134,7 +138,8 @@ function renderPackageJson(options) {
|
|
|
134
138
|
}, null, 2)}\n`;
|
|
135
139
|
}
|
|
136
140
|
function developmentSection(options) {
|
|
137
|
-
|
|
141
|
+
const isTarget = createIsTarget(options.target);
|
|
142
|
+
if (isTarget('aws-lambda')) {
|
|
138
143
|
return [
|
|
139
144
|
'## Development',
|
|
140
145
|
'',
|
|
@@ -152,13 +157,14 @@ function developmentSection(options) {
|
|
|
152
157
|
runScriptCommand(options.packageManager, 'dev'),
|
|
153
158
|
'```',
|
|
154
159
|
'',
|
|
155
|
-
|
|
160
|
+
isTarget('cloudflare-workers')
|
|
156
161
|
? 'Wrangler starts a local Cloudflare Workers environment.'
|
|
157
162
|
: 'Open <http://127.0.0.1:3000> to receive a JSON response.',
|
|
158
163
|
].join('\n');
|
|
159
164
|
}
|
|
160
165
|
function deploymentSection(options) {
|
|
161
|
-
|
|
166
|
+
const isTarget = createIsTarget(options.target);
|
|
167
|
+
if (isTarget('cloudflare-workers')) {
|
|
162
168
|
return [
|
|
163
169
|
'## Deploy',
|
|
164
170
|
'',
|
|
@@ -167,7 +173,7 @@ function deploymentSection(options) {
|
|
|
167
173
|
'```',
|
|
168
174
|
].join('\n');
|
|
169
175
|
}
|
|
170
|
-
if (
|
|
176
|
+
if (isTarget('aws-lambda')) {
|
|
171
177
|
return [
|
|
172
178
|
'## Build',
|
|
173
179
|
'',
|
|
@@ -182,7 +188,7 @@ function deploymentSection(options) {
|
|
|
182
188
|
'## Production',
|
|
183
189
|
'',
|
|
184
190
|
'```sh',
|
|
185
|
-
...(
|
|
191
|
+
...(isTarget('deno')
|
|
186
192
|
? []
|
|
187
193
|
: [runScriptCommand(options.packageManager, 'build')]),
|
|
188
194
|
runScriptCommand(options.packageManager, 'start'),
|
|
@@ -200,6 +206,20 @@ async function replaceText(path, search, replacement) {
|
|
|
200
206
|
const content = await readFile(path, 'utf8');
|
|
201
207
|
await writeFile(path, content.replaceAll(search, replacement), 'utf8');
|
|
202
208
|
}
|
|
209
|
+
async function rewriteLocalImportExtensionsForDeno(directory) {
|
|
210
|
+
const entries = await readdir(directory, { withFileTypes: true });
|
|
211
|
+
await Promise.all(entries.map(async (entry) => {
|
|
212
|
+
const path = join(directory, entry.name);
|
|
213
|
+
if (entry.isDirectory()) {
|
|
214
|
+
await rewriteLocalImportExtensionsForDeno(path);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
if (!entry.isFile() || !entry.name.endsWith('.ts'))
|
|
218
|
+
return;
|
|
219
|
+
const content = await readFile(path, 'utf8');
|
|
220
|
+
await writeFile(path, content.replaceAll(/(from\s+['"]\.{1,2}\/[^'"]+)\.js(['"])/gu, '$1.ts$2'), 'utf8');
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
203
223
|
function workerNameFor(packageName) {
|
|
204
224
|
return (packageName
|
|
205
225
|
.replace(/[^a-z0-9-]+/gu, '-')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-loutre",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Loutre project initializer",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"@clack/prompts": "^1.7.0",
|
|
21
|
-
"@loutrejs/loutre": "^0.
|
|
21
|
+
"@loutrejs/loutre": "^0.6.0",
|
|
22
22
|
"commander": "^15.0.0"
|
|
23
23
|
},
|
|
24
24
|
"repository": {
|
package/templates/base/README.md
CHANGED
|
@@ -4,6 +4,20 @@ A Loutre Application for {{targetLabel}}.
|
|
|
4
4
|
|
|
5
5
|
{{developmentSection}}
|
|
6
6
|
|
|
7
|
+
## Structure
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
src/
|
|
11
|
+
├ app.ts
|
|
12
|
+
├ app.test.ts
|
|
13
|
+
├ hello/
|
|
14
|
+
│ ├ contract.ts
|
|
15
|
+
│ └ controller.ts
|
|
16
|
+
└ main.ts
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Keep `app.ts` focused on root Application wiring. Group application code by feature or integration as the project grows.
|
|
20
|
+
|
|
7
21
|
## Verify
|
|
8
22
|
|
|
9
23
|
```sh
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { bootstrapApplication } from '@loutrejs/loutre'
|
|
2
|
+
import { bindHttpServer } from '@loutrejs/loutre/http'
|
|
2
3
|
import { expect, it } from 'vitest'
|
|
3
4
|
import application from './app.js'
|
|
4
5
|
|
|
5
6
|
it('GET / returns a response from the Loutre Application', async () => {
|
|
6
|
-
const app =
|
|
7
|
+
const app = await bootstrapApplication({
|
|
8
|
+
application,
|
|
9
|
+
capabilities: [bindHttpServer({ runtime: 'test' })],
|
|
10
|
+
})
|
|
7
11
|
try {
|
|
8
|
-
const response = await app.fetch(new Request('http://localhost/'))
|
|
12
|
+
const response = await app.http.fetch(new Request('http://localhost/'))
|
|
9
13
|
|
|
10
14
|
expect(response.status).toBe(200)
|
|
11
15
|
expect(await response.json()).toEqual({ message: 'Hello from Loutre!' })
|
|
@@ -1,55 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
defineApplication,
|
|
4
|
-
defineModule,
|
|
5
|
-
implementation,
|
|
6
|
-
} from '@loutrejs/loutre'
|
|
7
|
-
import { http } from '@loutrejs/loutre/http'
|
|
8
|
-
import { z } from 'zod'
|
|
9
|
-
|
|
10
|
-
const HelloContract = contract([
|
|
11
|
-
http({
|
|
12
|
-
hello: {
|
|
13
|
-
method: 'GET',
|
|
14
|
-
path: '/',
|
|
15
|
-
responses: {
|
|
16
|
-
ok: {
|
|
17
|
-
status: 200,
|
|
18
|
-
body: z.object({
|
|
19
|
-
message: z.string(),
|
|
20
|
-
}),
|
|
21
|
-
},
|
|
22
|
-
},
|
|
23
|
-
pipeline: [http.controller],
|
|
24
|
-
},
|
|
25
|
-
}),
|
|
26
|
-
])
|
|
27
|
-
|
|
28
|
-
const AppContract = contract([
|
|
29
|
-
http({
|
|
30
|
-
app: {
|
|
31
|
-
routes: HelloContract.http,
|
|
32
|
-
},
|
|
33
|
-
}),
|
|
34
|
-
])
|
|
35
|
-
|
|
36
|
-
const AppController = implementation({
|
|
37
|
-
name: 'AppController',
|
|
38
|
-
contract: AppContract.http.app.hello,
|
|
39
|
-
protocol: http,
|
|
40
|
-
factory: () => ({
|
|
41
|
-
async hello(ctx) {
|
|
42
|
-
return ctx.response.ok({
|
|
43
|
-
body: { message: 'Hello from Loutre!' },
|
|
44
|
-
})
|
|
45
|
-
},
|
|
46
|
-
}),
|
|
47
|
-
})
|
|
1
|
+
import { defineApplication, defineModule } from '@loutrejs/loutre'
|
|
2
|
+
import { AppController } from './hello/controller.js'
|
|
48
3
|
|
|
49
4
|
const AppModule = defineModule(() => ({
|
|
50
5
|
name: 'AppModule',
|
|
51
6
|
description: 'HTTP Application entry module',
|
|
52
|
-
|
|
7
|
+
executions: [AppController],
|
|
53
8
|
}))
|
|
54
9
|
|
|
55
10
|
export default defineApplication({
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { http } from '@loutrejs/loutre/http'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
export const AppContract = http.contract({
|
|
5
|
+
hello: {
|
|
6
|
+
method: 'GET',
|
|
7
|
+
path: '/',
|
|
8
|
+
responses: {
|
|
9
|
+
ok: {
|
|
10
|
+
status: 200,
|
|
11
|
+
body: z.object({
|
|
12
|
+
message: z.string(),
|
|
13
|
+
}),
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { http } from '@loutrejs/loutre/http'
|
|
2
|
+
import { AppContract } from './contract.js'
|
|
3
|
+
|
|
4
|
+
export const AppController = http.implementation({
|
|
5
|
+
name: 'AppController',
|
|
6
|
+
contract: AppContract,
|
|
7
|
+
factory: () => ({
|
|
8
|
+
async hello(ctx) {
|
|
9
|
+
return ctx.response.ok({
|
|
10
|
+
body: { message: 'Hello from Loutre!' },
|
|
11
|
+
})
|
|
12
|
+
},
|
|
13
|
+
}),
|
|
14
|
+
})
|
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"compilerOptions": {
|
|
3
3
|
"target": "ES2024",
|
|
4
|
+
"lib": [
|
|
5
|
+
"ES2024",
|
|
6
|
+
"DOM",
|
|
7
|
+
"WebWorker.ImportScripts",
|
|
8
|
+
"ScriptHost",
|
|
9
|
+
"DOM.Iterable",
|
|
10
|
+
"DOM.AsyncIterable",
|
|
11
|
+
"ESNext.Disposable"
|
|
12
|
+
],
|
|
4
13
|
"module": "NodeNext",
|
|
5
14
|
"moduleResolution": "NodeNext",
|
|
6
15
|
"strict": true,
|