create-loutre 0.4.1 → 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 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").TaskDescriptor<CreateProjectInput, CreateProjectResult, string>;
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
- }>], undefined, readonly [import("@loutrejs/loutre").TaskDescriptor<CreateProjectInput, CreateProjectResult, string>], readonly []>;
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, task } from '@loutrejs/loutre';
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 { bootstrap } from '@loutrejs/loutre/host';
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 = bootstrap({ application });
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
- return runScriptCommand(packageManager, target === 'aws-lambda' ? 'build' : 'dev');
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
- export declare const projectTargets: readonly ['node', 'bun', 'deno', 'cloudflare-workers', 'aws-lambda'];
2
- export type ProjectTarget = (typeof projectTargets)[number];
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 (options.target === 'cloudflare-workers') {
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
- if (options.target === 'aws-lambda') {
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
- options.target === 'cloudflare-workers'
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
- if (options.target === 'cloudflare-workers') {
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 (options.target === 'aws-lambda') {
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
- ...(options.target === 'deno'
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.4.1",
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.4.1",
21
+ "@loutrejs/loutre": "^0.6.0",
22
22
  "commander": "^15.0.0"
23
23
  },
24
24
  "repository": {
@@ -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 { bootstrap } from '@loutrejs/loutre/host'
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 = bootstrap({ application })
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,45 +1,12 @@
1
- import {
2
- contract,
3
- defineApplication,
4
- defineModule,
5
- implementation,
6
- } from '@loutrejs/loutre'
7
- import { http } from '@loutrejs/loutre/http'
8
- import { z } from 'zod'
9
- const AppContract = contract([
10
- http({
11
- hello: {
12
- method: 'GET',
13
- path: '/',
14
- responses: {
15
- ok: {
16
- status: 200,
17
- body: z.object({
18
- message: z.string(),
19
- }),
20
- },
21
- },
22
- pipeline: [http.controller],
23
- },
24
- }),
25
- ])
26
- const AppController = implementation({
27
- name: 'AppController',
28
- contract: AppContract,
29
- protocol: http,
30
- factory: () => ({
31
- async hello(ctx) {
32
- return ctx.response.ok({
33
- body: { message: 'Hello from Loutre!' },
34
- })
35
- },
36
- }),
37
- })
1
+ import { defineApplication, defineModule } from '@loutrejs/loutre'
2
+ import { AppController } from './hello/controller.js'
3
+
38
4
  const AppModule = defineModule(() => ({
39
5
  name: 'AppModule',
40
6
  description: 'HTTP Application entry module',
41
- implementations: [AppController],
7
+ executions: [AppController],
42
8
  }))
9
+
43
10
  export default defineApplication({
44
11
  modules: [AppModule()],
45
12
  })
@@ -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,