@lakutata/template-cli 3.0.0-beta.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/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2023-present Lakutata
4
+
5
+ All rights reserved
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,13 @@
1
+ # @lakutata/template-cli
2
+
3
+ The command line template of the [lakutata](https://github.com/lakutata/lakutata) projects: the actions of its controllers
4
+ are the commands of a tool (the commander adapter), built into executables which run without Node.js (`lakutata package
5
+ exe`, for this platform and the others), or bundled into one JavaScript file run by node (`lakutata package bundle`). The
6
+ project is a workspace: the tool is its app package, the packages it uses (native addons…) are next to it.
7
+
8
+ ```bash
9
+ npx @lakutata/cli create --name my-tool --template cli
10
+ ```
11
+
12
+ The files of the project are in `template/`; `lakutata create` fills `{$APP_NAME}`, `{$APP_ID}`, `{$APP_DESC}`,
13
+ `{$APP_AUTHOR}` and `{$APP_LICENSE}`, and renames `_gitignore` to `.gitignore`.
@@ -0,0 +1,6 @@
1
+ {
2
+ "scripts": {
3
+ "i18n:check": "lakutata i18n check --dir src/locales",
4
+ "i18n:types": "lakutata i18n types --dir src/locales"
5
+ }
6
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "greeting": {
3
+ "hello": "Hello, {name}!"
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "greeting": {
3
+ "hello": "你好,{name}!"
4
+ }
5
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "compilerOptions": {
3
+ "resolveJsonModule": true
4
+ }
5
+ }
@@ -0,0 +1,10 @@
1
+ # The installers of Linux (npm run package:linux), made on Linux: the packages of the configuration (lakutata.installer
2
+ # of the package.json) in dist/installer. The ones of another architecture: docker buildx build --platform linux/amd64 ...
3
+ # The apk packages are made on Alpine (node:22-alpine), with Bun when the application has native addons.
4
+ FROM node:22-bookworm AS build
5
+ WORKDIR /project
6
+ COPY . .
7
+ RUN (npm ci --no-audit --no-fund || npm install --no-audit --no-fund) && npm run build && npm run package -w app -- --outdir /installer
8
+
9
+ FROM scratch
10
+ COPY --from=build /installer /
@@ -0,0 +1,12 @@
1
+ .git
2
+ node_modules
3
+ **/node_modules
4
+ **/build
5
+ *.log
6
+ .env
7
+ .env.*
8
+ **/.next
9
+ **/.nuxt
10
+ **/.output
11
+ dist
12
+ **/dist
@@ -0,0 +1,6 @@
1
+ {
2
+ "scripts": {
3
+ "package:host": "npm run build && npm run package -w app -- --outdir ../../dist/installer",
4
+ "package:linux": "docker buildx build --file Dockerfile.installer --output type=local,dest=dist/installer ."
5
+ }
6
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "scripts": {
3
+ "package": "lakutata package installer"
4
+ },
5
+ "lakutata": {
6
+ "installer": {
7
+ "linux": {
8
+ "targets": [
9
+ "deb",
10
+ "rpm",
11
+ "tar.gz"
12
+ ]
13
+ }
14
+ }
15
+ }
16
+ }
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@lakutata/template-cli",
3
+ "version": "3.0.0-beta.0",
4
+ "description": "The command line template of the lakutata projects: a command line tool built into executables which run without Node.js.",
5
+ "keywords": [
6
+ "lakutata",
7
+ "lakutata-template"
8
+ ],
9
+ "license": "MIT",
10
+ "files": [
11
+ "template",
12
+ "features",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "lakutata": {
17
+ "template": "cli",
18
+ "features": {
19
+ "i18n": "The messages in several languages (lakutata/com/i18n): src/locales, the language of each request",
20
+ "installer": "The installers of the application (lakutata package installer): deb, rpm and tar.gz on Linux (npm run package:linux in Docker), pkg and tar.gz on macOS, nsis, msi and zip on Windows, its program in the PATH"
21
+ }
22
+ }
23
+ }
@@ -0,0 +1,89 @@
1
+ # {$APP_NAME}
2
+
3
+ {$APP_DESC}
4
+
5
+ A [lakutata](https://github.com/lakutata/lakutata) command line tool: the actions of its controllers are its commands.
6
+ It is built into an executable which runs without Node.js. The project is a workspace: `packages/app` is the tool, the
7
+ other packages are the ones it uses (`npx lakutata add <name>`: a package of TypeScript, or a native addon with
8
+ `--native`).
9
+
10
+ ```bash
11
+ npm run build # build the packages, in the order of their dependencies: node packages/app/build/App.js greet --name you
12
+ npm test # the tests of the packages
13
+ npm run package:exe # the executable of this platform: dist/{$APP_ID} (dist/{$APP_ID}.exe on Windows)
14
+ ./dist/{$APP_ID} greet --name you # run a command (--help lists them)
15
+ npm run package:bundle # one JavaScript file with the dependencies: node dist/{$APP_ID}.cjs greet --name you
16
+ ```
17
+
18
+ - `packages/app/src/App.ts` starts the tool;
19
+ - `packages/app/src/config/Config.ts` declares its components and the command line adapter (its name, its description
20
+ and its version come from `packages/app/package.json`);
21
+ - `packages/app/src/controllers/` holds the controllers: each `@CLIAction` is a command, whose options are the properties
22
+ of its DTO (`packages/app/src/dtos/`), validated before the action runs. In a terminal, the missing required options
23
+ are asked.
24
+
25
+ `npm link -w app` installs the command `{$APP_ID}` on this machine; `npm publish -w app` (named, without
26
+ `"private": true`) shares it.
27
+
28
+ ## The executables and the bundle
29
+
30
+ `lakutata package bundle` bundles the compiled tool and its dependencies into `dist/{$APP_ID}.cjs`, run by node without
31
+ `node_modules`. `lakutata package exe` embeds the bundle into the Node.js binary running it (the single executable
32
+ applications of Node.js): the executable runs without Node.js installed, the Node.js embedded is the version which makes
33
+ it.
34
+
35
+ - The native addons (`.node` files) are handled: next to the bundle (`dist/{$APP_ID}.native/`, distributed with it),
36
+ embedded in the executable, which extracts them at its first run into its cache directory (`LAKUTATA_NATIVE_DIR`
37
+ changes it).
38
+ - The bundle is a CommonJS module: no top-level `await` in the sources (await in an async function).
39
+ - The executables of macOS are signed ad hoc. The executables of Windows keep the signature of Node.js, which does not
40
+ match anymore: sign them before distributing them.
41
+
42
+ <!-- @if installer -->
43
+ ## The installers
44
+
45
+ `npm run package:host` makes the installers of the platform it runs on (`lakutata package installer`, configured by the
46
+ `lakutata.installer` of `packages/app/package.json`) in `dist/installer`, the command `{$APP_ID}` in the PATH:
47
+
48
+ - Linux: the deb, rpm and tar.gz packages installing the tool in `/opt/{$APP_ID}`; `npm run package:linux` makes them in
49
+ Docker (`Dockerfile.installer`) on any platform, `--platform linux/amd64` the ones of another architecture.
50
+ - macOS: the pkg package installing it in `/usr/local/lib/{$APP_ID}` (`sudo /usr/local/lib/{$APP_ID}/uninstall` removes it).
51
+ - Windows: the installer of NSIS and the package of Windows Installer (msi) installing it in `C:\Program Files\{$APP_ID}`.
52
+
53
+ <!-- @end -->
54
+ ## The other platforms
55
+
56
+ `npm run package:exe` makes the executable of the platform it runs on: the executables of the other platforms are made on them
57
+ (their native addons are built there, and they are tried there). A CI makes them all, for example GitHub Actions
58
+ (`.github/workflows/build.yml`):
59
+
60
+ ```yaml
61
+ name: build
62
+ on: push
63
+ jobs:
64
+ build:
65
+ strategy:
66
+ matrix:
67
+ os: [ubuntu-latest, ubuntu-24.04-arm, macos-latest, windows-latest]
68
+ runs-on: ${{ matrix.os }}
69
+ steps:
70
+ - uses: actions/checkout@v4
71
+ - uses: actions/setup-node@v4
72
+ with:
73
+ node-version: 22
74
+ - run: npm ci
75
+ - run: npm test
76
+ - run: npm run package:exe
77
+ - uses: actions/upload-artifact@v4
78
+ with:
79
+ name: {$APP_ID}-${{ runner.os }}-${{ runner.arch }}
80
+ path: dist/
81
+ ```
82
+
83
+ The Linux architectures are also made on any machine with Docker, in a container of the architecture (the project is
84
+ copied into it: the `node_modules` of the project are not changed):
85
+
86
+ ```bash
87
+ docker run --rm --platform linux/arm64 -v "$PWD":/source:ro -v "$PWD/dist":/source/dist node:22 sh -c \
88
+ "cp -r /source /project && cd /project && rm -rf node_modules && npm ci && npm run package:exe && cp dist/{$APP_ID} /source/dist/{$APP_ID}-linux-arm64"
89
+ ```
@@ -0,0 +1,13 @@
1
+ node_modules
2
+ build
3
+ dist
4
+ *.log
5
+ .env
6
+ .env.*
7
+ .DS_Store
8
+ .idea
9
+ .vscode
10
+ .next
11
+ next-env.d.ts
12
+ .nuxt
13
+ .output
@@ -0,0 +1,25 @@
1
+ {
2
+ "name": "{$APP_ID}-workspace",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "workspaces": [
6
+ "packages/*"
7
+ ],
8
+ "scripts": {
9
+ "build": "lakutata run rebuild",
10
+ "test": "lakutata run test",
11
+ "start": "npm start -w app",
12
+ "package:bundle": "npm run build && npm run package:bundle -w app -- --outfile ../../dist/{$APP_ID}.cjs",
13
+ "package:exe": "npm run build && npm run package:exe -w app -- --outdir ../../dist",
14
+ "version:patch": "lakutata version patch --yes"
15
+ },
16
+ "devDependencies": {
17
+ "@lakutata/builder": "^{$LAKUTATA_VERSION}",
18
+ "@lakutata/cli": "^{$LAKUTATA_VERSION}",
19
+ "@types/node": "^22.0.0",
20
+ "typescript": "^6.0.3"
21
+ },
22
+ "engines": {
23
+ "node": ">=22"
24
+ }
25
+ }
@@ -0,0 +1,34 @@
1
+ {
2
+ "name": "app",
3
+ "version": "0.0.1",
4
+ "description": "{$APP_DESC}",
5
+ "author": "{$APP_AUTHOR}",
6
+ "license": "{$APP_LICENSE}",
7
+ "private": true,
8
+ "type": "module",
9
+ "main": "build/App.js",
10
+ "bin": {
11
+ "{$APP_ID}": "build/App.js"
12
+ },
13
+ "files": [
14
+ "build",
15
+ "!build/tests"
16
+ ],
17
+ "scripts": {
18
+ "clean": "lakutata clean",
19
+ "build": "tsc",
20
+ "rebuild": "npm run clean && npm run build",
21
+ "start": "node build/App.js",
22
+ "test": "node --test \"build/tests/**/*.spec.js\"",
23
+ "package:bundle": "lakutata package bundle",
24
+ "package:exe": "lakutata package exe"
25
+ },
26
+ "dependencies": {
27
+ "@inquirer/prompts": "^8.7.2",
28
+ "commander": "^15.0.0",
29
+ "lakutata": "^{$LAKUTATA_VERSION}"
30
+ },
31
+ "lakutata": {
32
+ "icon": "../resources/logo.png"
33
+ }
34
+ }
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import {Application} from 'lakutata'
3
+ import {installerConfig} from './config/Config.js'
4
+
5
+ Application.run(installerConfig())
@@ -0,0 +1,36 @@
1
+ import type {ApplicationOptions} from 'lakutata'
2
+ import {buildEntrypoints} from 'lakutata/com/entrypoint'
3
+ import {commanderCLIEntrypoint} from 'lakutata/adapter/commander'
4
+ import manifest from '../../package.json' with {type: 'json'}
5
+ import {ExampleController} from '../controllers/ExampleController.js'
6
+ //@if i18n
7
+ import {buildI18nOptions, I18nMiddleware} from 'lakutata/com/i18n'
8
+ import en from '../locales/en.json' with {type: 'json'}
9
+ import zhCN from '../locales/zh-CN.json' with {type: 'json'}
10
+ //@end
11
+
12
+ /**
13
+ * The options of the tool
14
+ */
15
+ export function installerConfig(): ApplicationOptions {
16
+ return {
17
+ id: '{$APP_ID}',
18
+ name: '{$APP_NAME}',
19
+ components: {
20
+ //@if i18n
21
+ //The messages of the languages (src/locales), bundled with the code; created by the first request
22
+ i18n: buildI18nOptions({messages: {'en': en, 'zh-CN': zhCN}}),
23
+ //@end
24
+ entrypoint: buildEntrypoints({
25
+ controllers: [ExampleController],
26
+ //@if i18n
27
+ //The messages in the language of its environment (LANG)
28
+ middlewares: [I18nMiddleware],
29
+ //@end
30
+ //The actions of the controllers are the commands; the missing required options are asked in a terminal
31
+ cli: commanderCLIEntrypoint({bin: '{$APP_ID}', description: manifest.description, version: manifest.version})
32
+ })
33
+ },
34
+ bootstrap: ['entrypoint']
35
+ }
36
+ }
@@ -0,0 +1,32 @@
1
+ import {Controller} from 'lakutata/com/entrypoint'
2
+ import {CLIAction} from 'lakutata/decorator/ctrl'
3
+ import {GreetDTO} from '../dtos/GreetDTO.js'
4
+ //@if i18n
5
+ import {Inject} from 'lakutata/decorator/di'
6
+ import {Translator} from 'lakutata/com/i18n'
7
+ //@end
8
+
9
+ export class ExampleController extends Controller {
10
+ //@if i18n
11
+
12
+ /**
13
+ * The messages of the language of the request
14
+ */
15
+ @Inject(Translator)
16
+ protected readonly i18n: Translator
17
+ //@end
18
+
19
+ /**
20
+ * {$APP_ID} greet --name you: the result is printed
21
+ * @param input
22
+ */
23
+ @CLIAction('greet', GreetDTO, {description: 'Greet someone'})
24
+ public async greet(input: GreetDTO): Promise<string> {
25
+ //@if i18n
26
+ const greeting: string = this.i18n.t('greeting.hello', {name: input.name})
27
+ //@else
28
+ const greeting: string = `Hello, ${input.name}!`
29
+ //@end
30
+ return Array(input.times).fill(input.loud ? greeting.toUpperCase() : greeting).join('\n')
31
+ }
32
+ }
@@ -0,0 +1,16 @@
1
+ import {DTO} from 'lakutata'
2
+ import {Expect} from 'lakutata/decorator/dto'
3
+
4
+ /**
5
+ * The options of the command: --name <name> [--times <times>] [--loud]
6
+ */
7
+ export class GreetDTO extends DTO {
8
+ @Expect(DTO.String().required().description('Who is greeted'))
9
+ public name: string
10
+
11
+ @Expect(DTO.Number().integer().min(1).max(10).optional().default(1).description('How many times'))
12
+ public times: number
13
+
14
+ @Expect(DTO.Boolean().optional().default(false).description('Greet loudly'))
15
+ public loud: boolean
16
+ }
@@ -0,0 +1,66 @@
1
+ import {describe, it} from 'node:test'
2
+ //@if i18n
3
+ import {before} from 'node:test'
4
+ //@end
5
+ import assert from 'node:assert'
6
+ import {execFile, type ExecFileException} from 'node:child_process'
7
+ import {readFileSync} from 'node:fs'
8
+ import {fileURLToPath} from 'node:url'
9
+
10
+ /**
11
+ * The compiled tool
12
+ */
13
+ const TOOL: string = fileURLToPath(new URL('../App.js', import.meta.url))
14
+
15
+ /**
16
+ * Run the tool with arguments
17
+ * @param args
18
+ */
19
+ function tool(...args: string[]): Promise<{ code: number, stdout: string, stderr: string }> {
20
+ return new Promise((resolve: (result: { code: number, stdout: string, stderr: string }) => void): void => {
21
+ execFile(process.execPath, [TOOL, ...args], (error: ExecFileException | null, stdout: string, stderr: string): void => resolve({code: error ? (typeof error.code === 'number' ? error.code : 1) : 0, stdout: stdout, stderr: stderr}))
22
+ })
23
+ }
24
+
25
+ describe('{$APP_NAME}', (): void => {
26
+ //@if i18n
27
+
28
+ //The tool answers in the language of its environment: English for the tests
29
+ before((): void => {
30
+ process.env.LC_ALL = 'en_US.UTF-8'
31
+ })
32
+ //@end
33
+
34
+ it('greets', async (): Promise<void> => {
35
+ const result = await tool('greet', '--name', 'you')
36
+ assert.strictEqual(result.code, 0, result.stderr)
37
+ assert.strictEqual(result.stdout.trim(), 'Hello, you!')
38
+ })
39
+
40
+ //@if i18n
41
+ it('greets in the language of its environment', async (): Promise<void> => {
42
+ process.env.LC_ALL = 'zh_CN.UTF-8'
43
+ try {
44
+ assert.strictEqual((await tool('greet', '--name', 'you')).stdout.trim(), '你好,you!')
45
+ } finally {
46
+ process.env.LC_ALL = 'en_US.UTF-8'
47
+ }
48
+ })
49
+
50
+ //@end
51
+ it('greets with the options', async (): Promise<void> => {
52
+ const result = await tool('greet', '--name', 'you', '--times', '2', '--loud')
53
+ assert.strictEqual(result.stdout.trim(), 'HELLO, YOU!\nHELLO, YOU!')
54
+ })
55
+
56
+ it('refuses an invalid option', async (): Promise<void> => {
57
+ const result = await tool('greet', '--name', 'you', '--times', '11')
58
+ assert.notStrictEqual(result.code, 0)
59
+ assert.match(result.stderr, /times/)
60
+ })
61
+
62
+ it('shows its version', async (): Promise<void> => {
63
+ const {version} = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
64
+ assert.strictEqual((await tool('--version')).stdout.trim(), version)
65
+ })
66
+ })
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The package.json of the project, imported by the application for its name and its version: tsc keeps the import, node
3
+ * reads the file (build/ and src/ are next to it), the bundles embed it
4
+ */
5
+ declare module '*/package.json' {
6
+ const manifest: {
7
+ name: string
8
+ version: string
9
+ description?: string
10
+ }
11
+ export = manifest
12
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "lib": ["ES2022", "ESNext.Disposable"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "rootDir": "src",
8
+ "outDir": "build",
9
+ "types": ["node"],
10
+ "experimentalDecorators": true,
11
+ "emitDecoratorMetadata": true,
12
+ "useDefineForClassFields": false,
13
+ "strict": true,
14
+ "strictPropertyInitialization": false,
15
+ "esModuleInterop": true,
16
+ "skipLibCheck": true,
17
+ "sourceMap": true
18
+ },
19
+ "include": ["src"]
20
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "resources",
3
+ "version": "0.0.1",
4
+ "private": true,
5
+ "description": "The resources of the application: its icon (logo.png, lakutata.icon of the app package), made into the icons of each platform"
6
+ }