@nabilabs/create-builder 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/dist/index.js ADDED
@@ -0,0 +1,293 @@
1
+ // src/index.ts
2
+ import { spawn } from "node:child_process";
3
+ import { access, cp, lstat, readdir, readFile, rename, writeFile } from "node:fs/promises";
4
+ import { basename, dirname, resolve } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ // src/errors.ts
8
+ class CreateBuilderError extends Error {
9
+ constructor(message, options) {
10
+ super(message, options);
11
+ this.name = "CreateBuilderError";
12
+ }
13
+ }
14
+
15
+ // src/index.ts
16
+ var templatesDirectory = resolve(dirname(fileURLToPath(import.meta.url)), "../templates");
17
+ var packageManagerVariable = /\{\{packageManager\}\}/g;
18
+ var templateVariable = /\{\{projectName\}\}/g;
19
+ var packageManagerFromUserAgent = (userAgent) => {
20
+ const invokingUserAgent = userAgent ?? process.env.npm_config_user_agent;
21
+ if (invokingUserAgent?.startsWith("bun/"))
22
+ return "bun";
23
+ if (invokingUserAgent?.startsWith("pnpm/"))
24
+ return "pnpm";
25
+ if (invokingUserAgent?.startsWith("yarn/"))
26
+ return "yarn";
27
+ if (userAgent === undefined && process.versions.bun)
28
+ return "bun";
29
+ return "npm";
30
+ };
31
+ var packageManagerSpecificationFromUserAgent = (packageManager, userAgent) => {
32
+ const invokingUserAgent = userAgent ?? process.env.npm_config_user_agent;
33
+ const version = invokingUserAgent?.match(new RegExp(`(?:^|\\s)${packageManager}/([^\\s]+)`))?.[1];
34
+ const runtimeVersion = packageManager === "bun" ? process.versions.bun : undefined;
35
+ const resolvedVersion = version ?? runtimeVersion;
36
+ return resolvedVersion ? `${packageManager}@${resolvedVersion}` : packageManager;
37
+ };
38
+ var isMissing = (error) => error instanceof Error && ("code" in error) && error.code === "ENOENT";
39
+ var validateProjectName = (projectName) => {
40
+ const name = projectName.trim();
41
+ if (!name)
42
+ throw new CreateBuilderError("Project name cannot be empty.");
43
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(name)) {
44
+ throw new CreateBuilderError("Project name may only contain letters, numbers, dots, underscores, and hyphens.");
45
+ }
46
+ if (name === "." || name === "..")
47
+ throw new CreateBuilderError("Project name must name a new directory.");
48
+ return name;
49
+ };
50
+ var getTemplatePath = async (template) => {
51
+ if (!/^[a-z0-9][a-z0-9-]*$/i.test(template))
52
+ throw new CreateBuilderError(`Invalid template: ${template}`);
53
+ const path = resolve(templatesDirectory, template);
54
+ if (dirname(path) !== templatesDirectory)
55
+ throw new CreateBuilderError(`Invalid template: ${template}`);
56
+ try {
57
+ const stats = await lstat(path);
58
+ if (!stats.isDirectory())
59
+ throw new CreateBuilderError(`Template is not a directory: ${template}`);
60
+ } catch (error) {
61
+ if (isMissing(error))
62
+ throw new CreateBuilderError(`Unknown template: ${template}`);
63
+ throw error;
64
+ }
65
+ return path;
66
+ };
67
+ var getTemplateNames = async () => {
68
+ const entries = await readdir(templatesDirectory, { withFileTypes: true });
69
+ return entries.filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort((first, second) => first.localeCompare(second));
70
+ };
71
+ var validateTargetDirectory = async ({
72
+ cwd,
73
+ projectName
74
+ }) => {
75
+ const directory = resolve(cwd, projectName);
76
+ if (basename(directory) !== projectName)
77
+ throw new CreateBuilderError("Project name must not contain a path.");
78
+ try {
79
+ await access(directory);
80
+ } catch (error) {
81
+ if (isMissing(error))
82
+ return directory;
83
+ throw error;
84
+ }
85
+ throw new CreateBuilderError(`Directory already exists: ${directory}`);
86
+ };
87
+ var validateCurrentDirectory = async (directory) => {
88
+ const stats = await lstat(directory);
89
+ if (!stats.isDirectory())
90
+ throw new CreateBuilderError(`Current path is not a directory: ${directory}`);
91
+ const entries = await readdir(directory);
92
+ let hasPackageManifest = false;
93
+ for (const entry of entries) {
94
+ const path = resolve(directory, entry);
95
+ const entryStats = await lstat(path);
96
+ if (entry === "package.json" && entryStats.isFile()) {
97
+ hasPackageManifest = true;
98
+ continue;
99
+ }
100
+ if (entry === "node_modules" && entryStats.isDirectory())
101
+ continue;
102
+ throw new CreateBuilderError(`Current directory may only contain package.json and node_modules: ${directory}`);
103
+ }
104
+ return hasPackageManifest;
105
+ };
106
+ var resolveProjectTarget = async ({
107
+ cwd,
108
+ projectName
109
+ }) => {
110
+ if (projectName === "." || projectName === "./") {
111
+ const directory = resolve(cwd);
112
+ const hasPackageManifest = await validateCurrentDirectory(directory);
113
+ return {
114
+ directory,
115
+ hasPackageManifest,
116
+ isCurrentDirectory: true,
117
+ projectName: validateProjectName(basename(directory))
118
+ };
119
+ }
120
+ const name = validateProjectName(projectName);
121
+ return {
122
+ directory: await validateTargetDirectory({ cwd, projectName: name }),
123
+ hasPackageManifest: false,
124
+ isCurrentDirectory: false,
125
+ projectName: name
126
+ };
127
+ };
128
+ var copyTemplate = async ({
129
+ destination,
130
+ destinationExists = false,
131
+ skipPackageManifest = false,
132
+ source
133
+ }) => {
134
+ try {
135
+ if (destinationExists) {
136
+ const entries = (await readdir(source)).filter((entry) => !skipPackageManifest || entry !== "package.json");
137
+ await Promise.all(entries.map((entry) => cp(resolve(source, entry), resolve(destination, entry), {
138
+ errorOnExist: true,
139
+ force: false,
140
+ recursive: true
141
+ })));
142
+ } else {
143
+ await cp(source, destination, {
144
+ errorOnExist: true,
145
+ force: false,
146
+ recursive: true
147
+ });
148
+ }
149
+ await rename(resolve(destination, "gitignore"), resolve(destination, ".gitignore"));
150
+ } catch (error) {
151
+ if (error instanceof Error && "code" in error && error.code === "EEXIST") {
152
+ throw new CreateBuilderError(`Directory already exists: ${destination}`);
153
+ }
154
+ throw error;
155
+ }
156
+ };
157
+ var isManifest = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
158
+ var readPackageManifest = async (path) => {
159
+ try {
160
+ const manifest = JSON.parse(await readFile(path, "utf8"));
161
+ if (!isManifest(manifest))
162
+ throw new CreateBuilderError(`Invalid package.json: ${path}`);
163
+ return manifest;
164
+ } catch (error) {
165
+ if (error instanceof SyntaxError)
166
+ throw new CreateBuilderError(`Invalid package.json: ${path}`);
167
+ throw error;
168
+ }
169
+ };
170
+ var manifestSection = (manifest, section) => {
171
+ const value = manifest[section];
172
+ if (value === undefined)
173
+ return {};
174
+ if (!isManifest(value))
175
+ throw new CreateBuilderError(`Invalid package.json ${section}.`);
176
+ return value;
177
+ };
178
+ var mergePackageManifest = async ({
179
+ directory,
180
+ packageManagerSpecification,
181
+ projectName,
182
+ templatePath
183
+ }) => {
184
+ const path = resolve(directory, "package.json");
185
+ const [existing, templateManifest] = await Promise.all([
186
+ readPackageManifest(path),
187
+ readPackageManifest(resolve(templatePath, "package.json"))
188
+ ]);
189
+ const manifest = {
190
+ ...templateManifest,
191
+ ...existing,
192
+ devDependencies: {
193
+ ...manifestSection(existing, "devDependencies"),
194
+ ...manifestSection(templateManifest, "devDependencies")
195
+ },
196
+ name: projectName,
197
+ packageManager: packageManagerSpecification,
198
+ scripts: {
199
+ ...manifestSection(existing, "scripts"),
200
+ ...manifestSection(templateManifest, "scripts")
201
+ }
202
+ };
203
+ await writeFile(path, `${JSON.stringify(manifest, null, 2)}
204
+ `, "utf8");
205
+ };
206
+ var getFiles = async (directory) => {
207
+ const entries = await readdir(directory, { withFileTypes: true });
208
+ const paths = await Promise.all(entries.filter((entry) => entry.name !== "node_modules").map(async (entry) => {
209
+ const path = resolve(directory, entry.name);
210
+ if (entry.isDirectory())
211
+ return getFiles(path);
212
+ return [path];
213
+ }));
214
+ return paths.flat();
215
+ };
216
+ var replaceTemplateVariables = async ({
217
+ directory,
218
+ packageManagerSpecification,
219
+ projectName
220
+ }) => {
221
+ const files = await getFiles(directory);
222
+ await Promise.all(files.map(async (path) => {
223
+ const source = await readFile(path, "utf8");
224
+ const result = source.replace(templateVariable, projectName).replace(packageManagerVariable, packageManagerSpecification);
225
+ if (result !== source)
226
+ await writeFile(path, result, "utf8");
227
+ }));
228
+ };
229
+ var installDependencies = async (directory, packageManager) => {
230
+ await new Promise((resolve2, reject) => {
231
+ const child = spawn(packageManager, ["install"], {
232
+ cwd: directory,
233
+ shell: process.platform === "win32",
234
+ stdio: "inherit"
235
+ });
236
+ child.once("error", reject);
237
+ child.once("exit", (code) => {
238
+ if (code === 0)
239
+ return resolve2();
240
+ reject(new CreateBuilderError("Dependency installation failed."));
241
+ });
242
+ });
243
+ };
244
+ var createProject = async ({
245
+ cwd = process.cwd(),
246
+ install = installDependencies,
247
+ packageManager = "npm",
248
+ packageManagerSpecification = packageManager,
249
+ projectName,
250
+ template = "default"
251
+ }) => {
252
+ const target = await resolveProjectTarget({ cwd, projectName });
253
+ const source = await getTemplatePath(template);
254
+ if (target.hasPackageManifest)
255
+ await readPackageManifest(resolve(target.directory, "package.json"));
256
+ await copyTemplate({
257
+ destination: target.directory,
258
+ destinationExists: target.isCurrentDirectory,
259
+ skipPackageManifest: target.hasPackageManifest,
260
+ source
261
+ });
262
+ await replaceTemplateVariables({
263
+ directory: target.directory,
264
+ packageManagerSpecification,
265
+ projectName: target.projectName
266
+ });
267
+ if (target.hasPackageManifest) {
268
+ await mergePackageManifest({
269
+ directory: target.directory,
270
+ packageManagerSpecification,
271
+ projectName: target.projectName,
272
+ templatePath: source
273
+ });
274
+ }
275
+ await install(target.directory, packageManager);
276
+ return {
277
+ directory: target.directory,
278
+ projectName: target.projectName,
279
+ template
280
+ };
281
+ };
282
+ export {
283
+ validateTargetDirectory,
284
+ validateProjectName,
285
+ replaceTemplateVariables,
286
+ packageManagerSpecificationFromUserAgent,
287
+ packageManagerFromUserAgent,
288
+ installDependencies,
289
+ getTemplatePath,
290
+ getTemplateNames,
291
+ createProject,
292
+ copyTemplate
293
+ };
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@nabilabs/create-builder",
3
+ "version": "0.1.0",
4
+ "publisher": "Nabi Labs",
5
+ "displayName": "Nabi Builder - Create",
6
+ "description": "Create a new Nabi Builder project",
7
+ "type": "module",
8
+ "license": "MIT",
9
+ "packageManager": "bun",
10
+ "bin": {
11
+ "create-builder": "./dist/cli.js"
12
+ },
13
+ "scripts": {
14
+ "build": "bun run scripts/build.ts",
15
+ "----------": "----------",
16
+ "lint": "eslint . --fix",
17
+ "lint:check": "eslint . --max-warnings 0",
18
+ "format": "prettier --write .",
19
+ "format:check": "prettier --check .",
20
+ "typecheck": "bunx tsc --noEmit",
21
+ "test": "bun test",
22
+ "pack:check": "bun pm pack --dry-run --ignore-scripts",
23
+ "-----------": "-----------",
24
+ "fix": "bun run lint && bun run format",
25
+ "check": "bun run typecheck && bun run lint:check && bun run format:check && bun run test && bun run build && bun run pack:check",
26
+ "------------": "------------",
27
+ "prepack": "bun run build",
28
+ "prepublishOnly": "bun run check",
29
+ "prepare": "bun run scripts/prepare.ts"
30
+ },
31
+ "engines": {
32
+ "node": ">=22"
33
+ },
34
+ "devDependencies": {
35
+ "@eslint/js": "^9.39.0",
36
+ "@types/bun": "^1.4.0",
37
+ "@types/node": "^22.20.1",
38
+ "dts-bundle-generator": "^9.5.1",
39
+ "eslint": "^9.39.0",
40
+ "eslint-config-prettier": "^10.1.8",
41
+ "eslint-import-resolver-typescript": "^4.4.5",
42
+ "eslint-plugin-import": "^2.32.0",
43
+ "eslint-plugin-perfectionist": "^5.10.1",
44
+ "eslint-plugin-simple-import-sort": "^12.1.1",
45
+ "globals": "^17.4.0",
46
+ "husky": "^9.1.7",
47
+ "jiti": "^2.7.0",
48
+ "prettier": "^3.8.1",
49
+ "typescript": "5.9.3",
50
+ "typescript-eslint": "^8.68.0"
51
+ },
52
+ "keywords": [
53
+ "nabi",
54
+ "builder",
55
+ "html",
56
+ "ssg",
57
+ "static-site-generator",
58
+ "components",
59
+ "build-tool",
60
+ "create"
61
+ ],
62
+ "files": [
63
+ "dist",
64
+ "templates",
65
+ "README.md",
66
+ "LICENSE"
67
+ ],
68
+ "publishConfig": {
69
+ "access": "public",
70
+ "registry": "https://registry.npmjs.org/"
71
+ },
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "git+https://github.com/nabilabshq/create-builder.git"
75
+ },
76
+ "bugs": {
77
+ "url": "https://github.com/nabilabshq/create-builder/issues"
78
+ },
79
+ "homepage": "https://github.com/nabilabshq/create-builder#readme"
80
+ }
@@ -0,0 +1,12 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ indent_size = 2
7
+ indent_style = space
8
+ insert_final_newline = true
9
+ trim_trailing_whitespace = true
10
+
11
+ [*.md]
12
+ trim_trailing_whitespace = false
@@ -0,0 +1,6 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": false,
4
+ "tabWidth": 2,
5
+ "trailingComma": "all"
6
+ }
@@ -0,0 +1,22 @@
1
+ # {{projectName}}
2
+
3
+ Two-page static website built with [Nabi Builder](https://github.com/nabilabshq/builder).
4
+
5
+ ## Commands
6
+
7
+ ```bash
8
+ npm run dev
9
+ npm run build
10
+ npm run check
11
+ ```
12
+
13
+ With pnpm, use `pnpm run <script>`; with Yarn, use `yarn <script>`.
14
+
15
+ Start in `src/pages/index.html`. The template also includes the global `ui/head` component and `shared/styles/normalize.css`.
16
+
17
+ ## Routes
18
+
19
+ - `/` — `src/pages/index.html`
20
+ - `/about` — `src/pages/about/index.html`
21
+
22
+ The template demonstrates shared UI components, colocated CSS, and assets referenced through `@assets`.
@@ -0,0 +1,15 @@
1
+ import js from "@eslint/js";
2
+ import globals from "globals";
3
+
4
+ export default [
5
+ { ignores: ["dist", "node_modules"] },
6
+ js.configs.recommended,
7
+ {
8
+ files: ["src/**/*.js"],
9
+ languageOptions: {
10
+ ecmaVersion: "latest",
11
+ globals: globals.browser,
12
+ sourceType: "module",
13
+ },
14
+ },
15
+ ];
@@ -0,0 +1,3 @@
1
+ node_modules/
2
+ dist/
3
+ .nabi*
@@ -0,0 +1,7 @@
1
+ /** @type {import('@nabilabs/builder').NabiConfigInput} */
2
+ export default {
3
+ minify: {
4
+ css: true,
5
+ html: true,
6
+ },
7
+ };
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "packageManager": "{{packageManager}}",
6
+ "type": "module",
7
+ "scripts": {
8
+ "dev": "nabi dev",
9
+ "build": "nabi build",
10
+ "build:inline": "nabi build --mode inline",
11
+ "lint": "eslint . && stylelint \"src/**/*.css\"",
12
+ "format": "prettier . --write",
13
+ "check": "eslint . --max-warnings 0 && stylelint \"src/**/*.css\" && prettier . --check"
14
+ },
15
+ "devDependencies": {
16
+ "@eslint/js": "^9.39.0",
17
+ "@nabilabs/builder": "^0.2.0",
18
+ "eslint": "^9.39.0",
19
+ "globals": "^17.4.0",
20
+ "prettier": "^3.8.1",
21
+ "stylelint": "^16.24.0",
22
+ "stylelint-config-standard": "^39.0.0"
23
+ }
24
+ }
File without changes
@@ -0,0 +1,78 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <use
4
+ ref="ui/head"
5
+ title="About — {{projectName}}"
6
+ description="The second page of this Nabi Builder starter project."
7
+ >
8
+ <link use="base.css" />
9
+ </use>
10
+
11
+ <body>
12
+ <use ref="ui/header" active="about" />
13
+
14
+ <main class="shell about">
15
+ <section class="about__hero">
16
+ <p class="eyebrow">About this sample</p>
17
+ <h1>
18
+ Multi-page
19
+ <span>without the machinery.</span>
20
+ </h1>
21
+
22
+ <p class="about__lead">
23
+ This page exists because there is an
24
+ <code>about/index.html</code> file. The shared header and footer are
25
+ resolved during the build, not in the browser.
26
+ </p>
27
+ </section>
28
+
29
+ <section class="principles">
30
+ <article class="principle principle--wide">
31
+ <div>
32
+ <span class="principle__index">01</span>
33
+ <p class="principle__label">Routing</p>
34
+ </div>
35
+
36
+ <div>
37
+ <h2>Folders become URLs.</h2>
38
+ <p>
39
+ Put a page at <code>src/pages/about/index.html</code> and Nabi
40
+ emits the <code>/about</code> route.
41
+ </p>
42
+ </div>
43
+ </article>
44
+
45
+ <article class="principle">
46
+ <span class="principle__index">02</span>
47
+ <p class="principle__label">Components</p>
48
+ <h2>Shared once.</h2>
49
+ <p>
50
+ The same <code>ui/header</code> component is used on both pages with
51
+ a different <code>active</code> prop.
52
+ </p>
53
+ </article>
54
+
55
+ <article class="principle principle--accent">
56
+ <span class="principle__index">03</span>
57
+ <p class="principle__label">Runtime</p>
58
+ <strong>0</strong>
59
+ <p>No client-side component runtime is required to render this UI.</p>
60
+ </article>
61
+ </section>
62
+
63
+ <section class="back">
64
+ <div>
65
+ <p class="eyebrow">That is the whole demo</p>
66
+ <h2>Small enough to read in a minute.</h2>
67
+ </div>
68
+
69
+ <a href="/">
70
+ Back home
71
+ <span aria-hidden="true">↗</span>
72
+ </a>
73
+ </section>
74
+ </main>
75
+
76
+ <use ref="ui/footer" />
77
+ </body>
78
+ </html>