@reflex-stack/tsp 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/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # TSP
2
+
3
+ **TypeScript Package** (tsp), scaffolds and build **Typescript** sources to **EcmaScript modules** and publish them as modular packages to **NPM** or **JSR**.
4
+
5
+ ## Init a new library
6
+
7
+ First, create the associated **git repository** and clone it.
8
+
9
+ Run this command in git trunk :
10
+ ```bash
11
+ npx @reflex-stack/tsp init
12
+ ```
13
+
14
+ ## Created files
15
+
16
+ ```
17
+ ├─ dist/
18
+ ├─ src/
19
+ │ ├─ submodule
20
+ │ │ └─ index.ts
21
+ │ └─ index.ts
22
+ ├─ tests/
23
+ │ └─ test.js
24
+ ├─ .gitignore
25
+ ├─ .npmignore
26
+ ├─ LICENCE ( if MIT )
27
+ ├─ package.json
28
+ ├─ package-lock.json
29
+ ├─ README.md
30
+ └─ tsconfig.json
31
+ ```
32
+
33
+ ## Available commands
34
+
35
+ #### Build sources
36
+ ```shell
37
+ npm run build
38
+ ```
39
+ - Will clear `./dist`, build sources from `.ts` files to `.js` and `.d.ts` files.
40
+ - Will generate size report and generate `./reports` directory with JSON and SVG files.
41
+
42
+ > Run `npm run build --noSizeReport`
43
+
44
+ #### Test
45
+ - `npm run test`
46
+ > Will clear `./dist`, build sources and run tests. No size report.
47
+
48
+ #### Publish
49
+ - `npm run publish`
50
+ > Will clear `./dist`, build sources and run tests, and start publish process.
51
+ > This will ask you how to upgrade package.json version, push to git and npm.
52
+
53
+
54
+ ## Size report
55
+ - TODO SVG doc
56
+ - TODO JSON doc
57
+
58
+ ## tsconfig
59
+ - TODO doc, explain forbidden properties
60
+
61
+ ## TSP config
62
+ - TODO tsp config
63
+
64
+ ## Next
65
+ - TODO config override from package
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@reflex-stack/tsp",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "bin": {
6
+ "tsp": "./src/cli.js"
7
+ },
8
+ "scripts": {
9
+ "publish": "node ./src/cli.js publish"
10
+ },
11
+ "author": "Alexis Bouhet",
12
+ "license": "MIT",
13
+ "description": "",
14
+ "dependencies": {
15
+ "@types/node": "^22.10.2",
16
+ "@zouloux/cli": "^0.2.8",
17
+ "@zouloux/files": "^3.0.4",
18
+ "brotli-size": "^4.0.0",
19
+ "stach": "^2.0.1",
20
+ "terser": "^5.37.0",
21
+ "typescript": "^5.7.2"
22
+ }
23
+ }
package/src/cli.js ADDED
@@ -0,0 +1,225 @@
1
+ #!/usr/bin/env sh
2
+ ':' //# comment; exec /usr/bin/env "${RUNTIME:-node}" "$0" "$@"
3
+
4
+
5
+ import { askInput, CLICommands, execAsync, nicePrint, oraTask, execSync, table, askList, newLine } from "@zouloux/cli";
6
+ import { build, clearOutput } from "./commands/build.js";
7
+ import { getUserPackageJson, naiveHumanFileSize, showIntroMessage } from "./utils.js";
8
+ import { cleanSizeReports, generateJSON, generateSVGs, sizeReport } from "./commands/size-report.js";
9
+ import { init } from "./commands/init.js";
10
+ import { getConfig } from "./config.js";
11
+ import { test } from "./commands/test.js";
12
+
13
+
14
+ // ----------------------------------------------------------------------------- COMMANDS
15
+
16
+ const commands = new CLICommands({
17
+ noSizeReport: false,
18
+ noIntro: false,
19
+ })
20
+
21
+ // Executed for all commands
22
+ commands.before((args, flags, commandName) => {
23
+ if ( commandName !== "publish" && !flags.noIntro)
24
+ showIntroMessage(!commandName || commandName === "init")
25
+ })
26
+
27
+ /**
28
+ * INIT COMMAND
29
+ * Create a new ecma-build library ready to be published.
30
+ */
31
+ commands.add("init", async (args, flags, commandName) => {
32
+ await init()
33
+ })
34
+
35
+ /**
36
+ * BUILD COMMAND
37
+ * options: --noSizeReport
38
+ */
39
+ commands.add("build", async (args, flags) => {
40
+
41
+ const userPackage = getUserPackageJson()
42
+ const config = getConfig(userPackage)
43
+
44
+ await oraTask('Cleaning output', async ( task ) => {
45
+ await clearOutput( config )
46
+ task.success()
47
+ })
48
+
49
+ await oraTask('Building with tsc', async ( task ) => {
50
+ await build( config )
51
+ task.success('Built ✨')
52
+ }, (error, task) => {
53
+ task.error()
54
+ console.log(error.stdout ?? '')
55
+ console.log(error.stderr ?? '')
56
+ })
57
+
58
+ // CLI Flag to disable reports and only build
59
+ if ( flags.noSizeReport )
60
+ return
61
+
62
+ // Generate size report
63
+ const report = await oraTask('Generating size report', async ( task ) => {
64
+ // Extract "exports" from package.json and use them as bundle directories
65
+ if ( typeof userPackage.exports !== "object" )
66
+ nicePrint(`{b/r}Invalid export property in package.json.`)
67
+ const bundles = Object.keys( userPackage.exports )
68
+ // Generate report data
69
+ let report = await sizeReport( bundles, config )
70
+ task.success()
71
+ return report
72
+ })
73
+
74
+ // Generate
75
+ if ( config["generate-svg-report"] || config["generate-json-report"] ) {
76
+ await oraTask('Generating report files', async ( task ) => {
77
+ // Clean report directory
78
+ await cleanSizeReports( config )
79
+ // Generate JSON
80
+ if ( config["generate-json-report"] )
81
+ generateJSON( report, config )
82
+ // Generate SVG files
83
+ if ( config["generate-svg-report"] )
84
+ await generateSVGs( report, config )
85
+ })
86
+ }
87
+
88
+ // Print report table in CLI
89
+ const tableData = [
90
+ // Table header
91
+ ["Bundle", "Original size", "Brotli size"]
92
+ ]
93
+ // Table helpers
94
+ const tablePrint = s => nicePrint(s, { output: "return", newLine: false })
95
+ const tableLine = () => tableData.push(['', '', ''])
96
+ // Total bundle size
97
+ let totalOriginalSize = 0
98
+ let totalBrotliSize = 0
99
+ // Browse report bundles
100
+ report.forEach( bundleReport => {
101
+ tableLine()
102
+ // Count total bundle size
103
+ totalOriginalSize += bundleReport.sizes[0]
104
+ totalBrotliSize += bundleReport.sizes[2]
105
+ // Show bundle report
106
+ const isMainModule = bundleReport.name === "main"
107
+ tableData.push([
108
+ userPackage.name + (isMainModule ? "" : `/${bundleReport.name}`),
109
+ tablePrint(`{b}${naiveHumanFileSize(bundleReport.sizes[0])}`),
110
+ tablePrint(`{c/b}${naiveHumanFileSize(bundleReport.sizes[2])}`),
111
+ ])
112
+ // Show sub-files reports
113
+ const totalFiles = bundleReport.files.length
114
+ bundleReport.files.map( (file, i) => {
115
+ tableData.push([
116
+ tablePrint(`{d}${i === totalFiles - 1 ? "└" : "├"}─ ${file.path}`),
117
+ tablePrint(`{d}${naiveHumanFileSize(file.sizes[0])}`),
118
+ tablePrint(`{d}${naiveHumanFileSize(file.sizes[2])}`),
119
+ ])
120
+ })
121
+ })
122
+ // Show total if more than 1 bundle
123
+ if ( report.length > 1 ) {
124
+ tableLine()
125
+ tableData.push([
126
+ tablePrint(`{b}Total`),
127
+ tablePrint(`{b}${naiveHumanFileSize(totalOriginalSize)}`),
128
+ tablePrint(`{g/b}${naiveHumanFileSize(totalBrotliSize)}`),
129
+ ])
130
+ }
131
+ // Print table
132
+ newLine()
133
+ table(tableData, true, [20], ' ')
134
+ newLine()
135
+ })
136
+
137
+ /**
138
+ * TEST COMMAND
139
+ */
140
+ commands.add("test", async (args, flags, commandName) => {
141
+
142
+ const userPackage = getUserPackageJson()
143
+ const config = getConfig(userPackage)
144
+
145
+ await test( config )
146
+ })
147
+
148
+ commands.add("publish", async (args, flags, commandName) => {
149
+ // Check NPM connected user
150
+ await oraTask({text: `Connecting to npm`}, async task => {
151
+ try {
152
+ const whoami = await execAsync(`npm whoami`, 0)
153
+ task.success(nicePrint(`Hello {b/c}@${whoami}`, {output: 'return'}).trim())
154
+ return whoami
155
+ }
156
+ catch (e) {
157
+ task.error(`Please connect to npm with ${chalk.bold('npm login')}`)
158
+ }
159
+ })
160
+ // TODO : When test will build only needed files, move build after tests
161
+ // (to build all files after test has succeed)
162
+ // Compile
163
+ //await CLICommands.run(`build`, cliArguments, cliOptions)
164
+ //await CLICommands.run(`test`, cliArguments, cliOptions)
165
+
166
+ // todo : Internal build
167
+ // todo : Run test command
168
+ // todo : Internal run report
169
+ // Prepare commands
170
+ const userPackage = getUserPackageJson()
171
+ let { version, name } = userPackage
172
+ const config = getConfig( userPackage )
173
+ const libraryExecOptions = { cwd: config.cwd };
174
+ const stdioLevel = 3;
175
+ // Test this library, and exit if it fails
176
+ // Test passed, show current version and git status
177
+ newLine()
178
+ nicePrint(`📦 Current version of {b/c}${name}{/} is {b/c}${version}`)
179
+ // Ask how to increment version
180
+ const increment = await askList(`How to increment ?`, {
181
+ patch: 'patch (0.0.X) - No new features, patch bugs or optimize code',
182
+ minor: 'minor (0.X.0) - No breaking change, have new or improved features',
183
+ major: 'major (X.0.0) - Breaking change',
184
+ // Keep but publish on NPM (if already increment in package.json)
185
+ keep: `keep (${ version }) - Publish current package.json version`,
186
+ // Push on git but no lib publish
187
+ push: `push - Push on git only, no npm publish`,
188
+ // Skip this lib (no publish at all, go to next library)
189
+ skip: `skip - Do not publish ${ name }`,
190
+ }, { returnType: 'key' });
191
+ // Go to next library
192
+ if ( increment === 'skip' )
193
+ return
194
+ // execSync(`git status -s`, stdioLevel, libraryExecOptions)
195
+ // Ask for commit message
196
+ let message = await askInput(`Commit message ?`);
197
+ message = message.replace(/["']/g, "'");
198
+ // If we increment, use npm version
199
+ if ( increment !== 'keep' && increment !== 'push' ) {
200
+ version = execSync(`npm version ${increment} --no-git-tag-version -m"${name} - %s - ${message}"`, stdioLevel, libraryExecOptions).toString().trim();
201
+ }
202
+ // Add to git and push
203
+ execSync(`git add .`, stdioLevel, libraryExecOptions);
204
+ execSync(`git commit -m"${name} - ${version} : ${message}"`, stdioLevel, libraryExecOptions);
205
+ execSync(`git push`, stdioLevel, libraryExecOptions);
206
+ // Publish on npm as public
207
+ // FIXME : Access public as an option for private repositories
208
+ // Ingore script to avoid infinite loop (if "package.json.scripts.publish" == "tsbundle publish")
209
+ if ( increment !== 'push' ) {
210
+ execSync(`npm publish --access public --ignore-scripts`, stdioLevel, libraryExecOptions);
211
+ nicePrint(`👌 {b/g}${name}{/}{g} Published, new version is {b/g}${version}`)
212
+ }
213
+ else {
214
+ nicePrint(`👍 {b/g}${name}{/}{g} Pushed to git`)
215
+ }
216
+ })
217
+
218
+ commands.start(function (commandName) {
219
+ if ( commandName )
220
+ return
221
+ nicePrint(`{b/r}Command '${commandName}' not found`)
222
+ newLine()
223
+ nicePrint(`Available commands :`)
224
+ commands.list().forEach( command => nicePrint(`- ${command}`) )
225
+ })
@@ -0,0 +1,15 @@
1
+ import { execStream } from "@zouloux/cli";
2
+ import { Directory } from "@zouloux/files";
3
+ import { join } from "node:path";
4
+
5
+
6
+ export async function clearOutput ( config ) {
7
+ const dir = new Directory( join(config.cwd, config.dist) )
8
+ await dir.ensureParents()
9
+ await dir.clean()
10
+ }
11
+
12
+ export async function build ( config ) {
13
+ const command = `tsc -p tsconfig.json --rootDir ${config.src} --outDir ${config.dist} --declaration true --noEmitOnError true --pretty`
14
+ await execStream(command, { cwd: config.cwd }, () => {})
15
+ }
@@ -0,0 +1,245 @@
1
+ import { nicePrint, askInput, askList, newLine, oraTask, execAsync } from "@zouloux/cli"
2
+ import { getConfig } from "../config.js";
3
+ import { getGitRemoteUrl, getTSPPackageJson } from "../utils.js";
4
+ import { existsSync, writeFileSync } from "node:fs";
5
+ import { Stach } from "stach";
6
+ import { mkdirSync } from "fs";
7
+ import { join } from "node:path";
8
+
9
+ const licenceTemplate = `MIT License
10
+
11
+ Copyright (c) 2022 {{ authorName }}
12
+
13
+ Permission is hereby granted, free of charge, to any person obtaining a copy
14
+ of this software and associated documentation files (the "Software"), to deal
15
+ in the Software without restriction, including without limitation the rights
16
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
17
+ copies of the Software, and to permit persons to whom the Software is
18
+ furnished to do so, subject to the following conditions:
19
+
20
+ The above copyright notice and this permission notice shall be included in all
21
+ copies or substantial portions of the Software.
22
+
23
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
24
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
25
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
26
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
27
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
28
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
29
+ SOFTWARE.`
30
+
31
+ const readmeTemplate = `# {{ packageTextName }}
32
+ Main bundle is
33
+ <picture style="display: inline-block">
34
+ <source media="(prefers-color-scheme: dark)" srcset="./reports/main-dark.svg">
35
+ <img src="./reports/main-light.svg">
36
+ </picture>
37
+ Optional submodule is only
38
+ <picture style="display: inline-block">
39
+ <source media="(prefers-color-scheme: dark)" srcset="./reports/submodule-dark.svg">
40
+ <img src="./reports/submodule-light.svg">
41
+ </picture>
42
+
43
+ ## Install
44
+
45
+ \`npm i {{ packageNPMName }}\`
46
+
47
+ ## Usage
48
+
49
+ ##### Main module
50
+ - \`import { ... } from "{{ packageNPMName }}"\`
51
+
52
+ ##### Sub module
53
+ - \`import { ... } from "{{ packageNPMName }}/submodule"\`
54
+
55
+ ## tsp commands
56
+
57
+ ##### Build
58
+ - \`npm run build\`
59
+ ##### Test
60
+ - \`npm run test\`
61
+ ##### Publish
62
+ - \`npm run publish\`
63
+ `
64
+
65
+ const tsconfigTemplate = `{
66
+ "compilerOptions": {
67
+ // Compilation level target
68
+ // If using a bundler in your project it should be the previous or current year
69
+ "target" : "{{esLevel}}",
70
+ // TS libs used, include needed lib.
71
+ // It should contain the target and "DOM" if it runs in the browser
72
+ // https://www.typescriptlang.org/tsconfig/#lib
73
+ "lib": [{{libs}}],
74
+ // Module config, you should not change it
75
+ "module": "NodeNext",
76
+ "isolatedModules": false,
77
+ "allowImportingTsExtensions": false,
78
+ // Compiler config
79
+ "strict": {{tsStrict}},
80
+ "allowJs": false,
81
+ "useDefineForClassFields" : true,
82
+ "allowSyntheticDefaultImports": true,
83
+ // ---
84
+ // Feel free to configure this file for your needs
85
+ // https://www.typescriptlang.org/tsconfig/
86
+ // ---
87
+ // Forbidden props are :
88
+ // - rootDir
89
+ // - outDir
90
+ // - declaration
91
+ // - noEmitOnError
92
+ // - pretty
93
+ }
94
+ }
95
+ `
96
+
97
+ const gitIgnoreTemplate = `.DS_Store
98
+ .idea
99
+ tmp
100
+ node_modules
101
+ dist
102
+ `
103
+
104
+ const npmIgnoreTemplate = `.DS_Store
105
+ .idea
106
+ LICENCE
107
+ tmp/
108
+ src/
109
+ .github/
110
+ docs/
111
+ `
112
+
113
+ const rootIndexTs = `// Root index file, export elements here
114
+ // Import nothing from submodule to keep things separated
115
+ export function randomFunction () {
116
+ return 5
117
+ }
118
+ `
119
+ const subModuleIndexTs = `// Sub-module index file, export elements here
120
+ // You can import elements from root
121
+
122
+ // Always import with the js extension, event in ts files
123
+ import { randomFunction } from "../index.js"
124
+
125
+ export function subRandomFunction () {
126
+ return randomFunction() * 12
127
+ }
128
+ `
129
+
130
+ const testFile = `// Write your test file here
131
+ console.log("No test implemented yet")
132
+ process.exit(0)
133
+ `
134
+
135
+ export async function init () {
136
+ // Check if some critical file already exists and warn before overriding
137
+ if ( existsSync("package.json") ) {
138
+ nicePrint(`{o}package.json is already existing. Continuing will override files in this directory.`)
139
+ const sure = await askList("Are you sure to continue ?", ["Yes", "No"], { defaultIndex: 1, returnType: "index" })
140
+ if ( sure === 1 )
141
+ return
142
+ }
143
+ // Get git remote
144
+ const config = getConfig()
145
+ let remoteURL = getGitRemoteUrl( config.cwd )
146
+ if ( !remoteURL )
147
+ nicePrint(`{o}Before scaffolding your TypeScript package, you should create the associated git repository.`)
148
+ else
149
+ nicePrint(`{d}Git origin is {/}${remoteURL}`)
150
+ newLine()
151
+ const options = { remoteURL }
152
+ options.packageTextName = await askInput(`Package name, in plain text {d}ex : My Package`, { notEmpty: true })
153
+ options.packageNPMName = await askInput(`Package name, for NPM, with namespace {d}ex : @mynamespace/mypackage`, { notEmpty: true })
154
+ options.authorName = await askInput(`Author name`, { notEmpty: true })
155
+ options.licenceName = await askInput(`Licence name`, { defaultValue: "MIT" })
156
+ options.esLevel = await askInput(`ES Level for tsconfig`, { defaultValue: "es2023" })
157
+ options.tsStrict = await askList(`Use strict Typescript?`, ["Yes", "No"], { defaultIndex: 1, returnType: "index" })
158
+ options.domAccess = await askList(`Will it have access to DOM?`, ["Yes", "No"], { defaultIndex: 0, returnType: "index" })
159
+ options.svgReport = await askList(`Export SVG size report on build for README.md?`, ["Yes", "No"], { defaultIndex: 0, returnType: "index" })
160
+ options.jsonReport = await askList(`Export JSON size report on build?`, ["Yes", "No"], { defaultIndex: 1, returnType: "index" })
161
+ options.domAccess = options.domAccess === 0
162
+ options.tsStrict = options.tsStrict === 0
163
+ options.svgReport = options.svgReport === 0
164
+ options.jsonReport = options.jsonReport === 0
165
+ options.libs = [options.domAccess ? "DOM" : "", options.esLevel]
166
+ .filter( Boolean )
167
+ .map( s => `"${s}"`)
168
+ options.tspVersion = getTSPPackageJson().version
169
+ // Generate package json
170
+ const packageJson = {
171
+ name: options.packageNPMName,
172
+ version: "0.1.0",
173
+ type: "module",
174
+ author: options.authorName,
175
+ licence: options.licenceName,
176
+ main: "./dist/index.js",
177
+ types: "./dist/index.d.ts",
178
+ exports: {
179
+ ".": {
180
+ types: "./dist/index.d.ts",
181
+ default: "./dist/index.js"
182
+ },
183
+ "./submodule": {
184
+ types: "./dist/submodule/index.d.ts",
185
+ default: "./dist/submodule/index.js"
186
+ }
187
+ },
188
+ tsp: {
189
+ runtime: "node",
190
+ src: './src',
191
+ dist: './dist',
192
+ tests: './tests',
193
+ "test-files": ['test.js'],
194
+ tmp: './tmp',
195
+ reports: './reports',
196
+ "generate-json-report": options.jsonReport,
197
+ "generate-svg-report": options.svgReport
198
+ },
199
+ scripts: {
200
+ build: "tsp build",
201
+ test: "tsp build --noSizeReport && tsp test --noIntro",
202
+ publish: "tsp build && tsp test --noIntro && tsp publish --noIntro"
203
+ },
204
+ dependencies: {
205
+ "@reflex-stack/tsp": options.tspVersion
206
+ }
207
+ }
208
+ // Inject git remote
209
+ if ( options.remoteURL ) {
210
+ packageJson.repository = {
211
+ type: "git",
212
+ url: options.remoteURL
213
+ }
214
+ }
215
+ // Create directories
216
+ mkdirSync(config.tests, { recursive: true })
217
+ mkdirSync(config.dist, { recursive: true })
218
+ mkdirSync(join(config.src, "submodule"), { recursive: true })
219
+ // Generate root files
220
+ if ( options.licenceName === "MIT" )
221
+ writeFileSync("LICENCE", Stach(licenceTemplate, options))
222
+ writeFileSync("README.md", Stach(readmeTemplate, options))
223
+ writeFileSync(".gitignore", Stach(gitIgnoreTemplate, options))
224
+ writeFileSync(".npmignore", Stach(npmIgnoreTemplate, options))
225
+ writeFileSync("tsconfig.json", Stach(tsconfigTemplate, options))
226
+ writeFileSync("package.json", JSON.stringify(packageJson, null, 2))
227
+ // Generate src files
228
+ writeFileSync(join(config.src, "index.ts"), Stach(rootIndexTs, options))
229
+ writeFileSync(join(config.src, "submodule", "index.ts"), Stach(subModuleIndexTs, options))
230
+ // Generate test file
231
+ writeFileSync(join(config.tests, "test.js"), Stach(testFile, options))
232
+ // Install dependencies
233
+ await oraTask("Installing dependencies", async () => {
234
+ await execAsync(`npm i typescript terser`, false, { cwd: config.cwd })
235
+ })
236
+ // Show commands
237
+ newLine()
238
+ nicePrint(`{b/g}Package ${options.packageNPMName} created ✨`)
239
+ newLine()
240
+ nicePrint(`Available commands :`)
241
+ nicePrint(`- npm run build`)
242
+ nicePrint(`- npm run test`)
243
+ nicePrint(`- npm run publish`)
244
+ newLine()
245
+ }