create-nudge-ui 0.1.2
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 +24 -0
- package/dist/LICENSE +21 -0
- package/dist/arguments.d.ts +10 -0
- package/dist/arguments.js +55 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +91 -0
- package/dist/configuration.d.ts +5 -0
- package/dist/configuration.js +269 -0
- package/dist/detection.d.ts +11 -0
- package/dist/detection.js +141 -0
- package/dist/entry.d.ts +2 -0
- package/dist/entry.js +6 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/installer.d.ts +10 -0
- package/dist/installer.js +24 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.js +2 -0
- package/package.json +49 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# `create-nudge-ui`
|
|
2
|
+
|
|
3
|
+
`create-nudge-ui` detects an application's host framework, installs the
|
|
4
|
+
corresponding Nudge UI adapter, and updates the host configuration.
|
|
5
|
+
|
|
6
|
+
Run the initializer from the application root:
|
|
7
|
+
|
|
8
|
+
```sh
|
|
9
|
+
npm create nudge-ui@latest
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
The initializer supports Next.js, Astro, Vite with React, and static HTML. Host
|
|
13
|
+
frameworks take precedence over their underlying tools, so an Astro project
|
|
14
|
+
with React islands receives `@nudge-ui/astro`, not `@nudge-ui/vite-react`.
|
|
15
|
+
|
|
16
|
+
Use an explicit framework when automatic detection is ambiguous:
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
npm create nudge-ui@latest -- --framework astro
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Use `--dry-run` to print the package installation and configuration change
|
|
23
|
+
without modifying the project. Run `npm create nudge-ui@latest -- --help` for
|
|
24
|
+
the complete command reference.
|
package/dist/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Charles Smart
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type Framework, type PackageManager } from "./types.js";
|
|
2
|
+
export interface CliOptions {
|
|
3
|
+
readonly framework?: Framework;
|
|
4
|
+
readonly packageManager?: PackageManager;
|
|
5
|
+
readonly dryRun: boolean;
|
|
6
|
+
readonly help: boolean;
|
|
7
|
+
}
|
|
8
|
+
/** Parses the initializer command line. */
|
|
9
|
+
export declare function parseArguments(args: readonly string[]): CliOptions;
|
|
10
|
+
export declare const helpText = "Set up Nudge UI in the current project.\n\nUsage: npm create nudge-ui@latest -- [options]\n\nOptions:\n --framework <nextjs|astro|vite-react|standalone>\n --package-manager <pnpm|npm|yarn|bun>\n --dry-run\n -h, --help";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { frameworks, packageManagers } from "./types.js";
|
|
2
|
+
/** Parses the initializer command line. */
|
|
3
|
+
export function parseArguments(args) {
|
|
4
|
+
let framework;
|
|
5
|
+
let packageManager;
|
|
6
|
+
let dryRun = false;
|
|
7
|
+
let help = false;
|
|
8
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9
|
+
const argument = args[index];
|
|
10
|
+
if (argument === "--")
|
|
11
|
+
continue;
|
|
12
|
+
if (argument === "--dry-run")
|
|
13
|
+
dryRun = true;
|
|
14
|
+
else if (argument === "--help" || argument === "-h")
|
|
15
|
+
help = true;
|
|
16
|
+
else if (argument === "--framework")
|
|
17
|
+
framework = parseFramework(requiredValue(args, ++index, argument));
|
|
18
|
+
else if (argument.startsWith("--framework="))
|
|
19
|
+
framework = parseFramework(argument.slice("--framework=".length));
|
|
20
|
+
else if (argument === "--package-manager") {
|
|
21
|
+
packageManager = parsePackageManager(requiredValue(args, ++index, argument));
|
|
22
|
+
}
|
|
23
|
+
else if (argument.startsWith("--package-manager=")) {
|
|
24
|
+
packageManager = parsePackageManager(argument.slice("--package-manager=".length));
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
throw new Error(`Unknown option: ${argument}`);
|
|
28
|
+
}
|
|
29
|
+
return { framework, packageManager, dryRun, help };
|
|
30
|
+
}
|
|
31
|
+
function requiredValue(args, index, option) {
|
|
32
|
+
const value = args[index];
|
|
33
|
+
if (!value || value.startsWith("-"))
|
|
34
|
+
throw new Error(`${option} requires a value.`);
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
37
|
+
export const helpText = `Set up Nudge UI in the current project.
|
|
38
|
+
|
|
39
|
+
Usage: npm create nudge-ui@latest -- [options]
|
|
40
|
+
|
|
41
|
+
Options:
|
|
42
|
+
--framework <nextjs|astro|vite-react|standalone>
|
|
43
|
+
--package-manager <pnpm|npm|yarn|bun>
|
|
44
|
+
--dry-run
|
|
45
|
+
-h, --help`;
|
|
46
|
+
function parseFramework(value) {
|
|
47
|
+
if (value === "nextjs" || value === "astro" || value === "vite-react" || value === "standalone")
|
|
48
|
+
return value;
|
|
49
|
+
throw new Error(`--framework must be one of: ${frameworks.join(", ")}.`);
|
|
50
|
+
}
|
|
51
|
+
function parsePackageManager(value) {
|
|
52
|
+
if (value === "pnpm" || value === "npm" || value === "yarn" || value === "bun")
|
|
53
|
+
return value;
|
|
54
|
+
throw new Error(`--package-manager must be one of: ${packageManagers.join(", ")}.`);
|
|
55
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { createInterface } from "node:readline/promises";
|
|
3
|
+
import { stdin, stdout } from "node:process";
|
|
4
|
+
import { basename, relative } from "node:path";
|
|
5
|
+
import { parseArguments, helpText } from "./arguments.js";
|
|
6
|
+
import { planConfiguration } from "./configuration.js";
|
|
7
|
+
import { adapterPackage, detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
|
|
8
|
+
import { formatCommand, installCommand, installPackage } from "./installer.js";
|
|
9
|
+
import { frameworks } from "./types.js";
|
|
10
|
+
/** Runs the Nudge UI project initializer. */
|
|
11
|
+
export async function runCli(args = process.argv.slice(2), projectRoot = process.cwd()) {
|
|
12
|
+
const options = parseArguments(args);
|
|
13
|
+
if (options.help) {
|
|
14
|
+
stdout.write(`${helpText}\n`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const manifest = readProjectManifest(projectRoot);
|
|
18
|
+
const detected = detectFrameworks(projectRoot, manifest);
|
|
19
|
+
const framework = options.framework ?? await selectFramework(detected);
|
|
20
|
+
const packageManager = options.packageManager ?? detectPackageManager(projectRoot, manifest);
|
|
21
|
+
const packageName = adapterPackage(framework);
|
|
22
|
+
const command = installCommand(packageManager, packageName);
|
|
23
|
+
const change = planConfiguration(projectRoot, framework);
|
|
24
|
+
const staticRoot = framework === "standalone" ? detectStaticRoot(projectRoot) : undefined;
|
|
25
|
+
const serveDirectory = staticRoot ? relative(projectRoot, staticRoot) || "." : ".";
|
|
26
|
+
stdout.write(`Nudge UI detected ${frameworkDisplayName(framework)} in ${basename(projectRoot)}.\n`);
|
|
27
|
+
if (options.dryRun) {
|
|
28
|
+
stdout.write(`Would run: ${formatCommand(command)}\n`);
|
|
29
|
+
if (change)
|
|
30
|
+
stdout.write(`Would ${change.created ? "create" : "update"}: ${change.path}\n`);
|
|
31
|
+
if (framework === "standalone")
|
|
32
|
+
stdout.write(`After installation, run: nudge-ui serve ${serveDirectory}\n`);
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (change) {
|
|
36
|
+
writeFileSync(change.path, change.content);
|
|
37
|
+
}
|
|
38
|
+
try {
|
|
39
|
+
installPackage(command, projectRoot);
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
if (change)
|
|
43
|
+
rollbackConfiguration(change);
|
|
44
|
+
throw error;
|
|
45
|
+
}
|
|
46
|
+
if (change) {
|
|
47
|
+
stdout.write(`${change.created ? "Created" : "Updated"} ${change.path}.\n`);
|
|
48
|
+
}
|
|
49
|
+
if (framework === "standalone") {
|
|
50
|
+
stdout.write(`Run \`nudge-ui serve ${serveDirectory}\` to start the instrumented development server.\n`);
|
|
51
|
+
}
|
|
52
|
+
stdout.write(`Nudge UI is configured with ${packageName}.\n`);
|
|
53
|
+
}
|
|
54
|
+
async function selectFramework(detected) {
|
|
55
|
+
if (detected.length === 1)
|
|
56
|
+
return detected[0];
|
|
57
|
+
if (!stdin.isTTY || !stdout.isTTY)
|
|
58
|
+
throw detectionError(detected);
|
|
59
|
+
const choices = detected.length > 1 ? detected : frameworks;
|
|
60
|
+
stdout.write("Select the host framework:\n");
|
|
61
|
+
choices.forEach((framework, index) => stdout.write(` ${index + 1}. ${frameworkDisplayName(framework)}\n`));
|
|
62
|
+
const reader = createInterface({ input: stdin, output: stdout });
|
|
63
|
+
try {
|
|
64
|
+
while (true) {
|
|
65
|
+
const answer = await reader.question("Framework: ");
|
|
66
|
+
const selected = choices[Number(answer) - 1];
|
|
67
|
+
if (selected)
|
|
68
|
+
return selected;
|
|
69
|
+
stdout.write(`Enter a number from 1 to ${choices.length}.\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
finally {
|
|
73
|
+
reader.close();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function rollbackConfiguration(change) {
|
|
77
|
+
if (change.created) {
|
|
78
|
+
rmSync(change.path, { force: true });
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (change.originalContent === undefined) {
|
|
82
|
+
throw new Error(`Could not restore ${change.path}: original content was not recorded.`);
|
|
83
|
+
}
|
|
84
|
+
writeFileSync(change.path, change.originalContent);
|
|
85
|
+
}
|
|
86
|
+
function detectionError(detected) {
|
|
87
|
+
if (detected.length > 1) {
|
|
88
|
+
return new Error(`Detected multiple host frameworks (${detected.join(", ")}). Pass --framework explicitly.`);
|
|
89
|
+
}
|
|
90
|
+
return new Error("Could not detect a supported host framework. Pass --framework explicitly.");
|
|
91
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { ConfigurationChange, Framework } from "./types.js";
|
|
2
|
+
/** Plans an idempotent host-configuration edit without writing to the project. */
|
|
3
|
+
export declare function planConfiguration(projectRoot: string, framework: Framework): ConfigurationChange | undefined;
|
|
4
|
+
/** Adds the selected adapter to a conventional host configuration source. */
|
|
5
|
+
export declare function configureSource(source: string, framework: Exclude<Framework, "standalone">, fileName: string): string;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import ts from "typescript";
|
|
4
|
+
const nextConfigNames = ["next.config.ts", "next.config.mts", "next.config.mjs", "next.config.js", "next.config.cjs"];
|
|
5
|
+
const astroConfigNames = ["astro.config.ts", "astro.config.mts", "astro.config.mjs", "astro.config.js"];
|
|
6
|
+
const viteConfigNames = ["vite.config.ts", "vite.config.mts", "vite.config.mjs", "vite.config.js"];
|
|
7
|
+
/** Plans an idempotent host-configuration edit without writing to the project. */
|
|
8
|
+
export function planConfiguration(projectRoot, framework) {
|
|
9
|
+
if (framework === "standalone")
|
|
10
|
+
return undefined;
|
|
11
|
+
const existingName = configurationNames(framework).find((name) => existsSync(join(projectRoot, name)));
|
|
12
|
+
const name = existingName ?? defaultConfigName(framework);
|
|
13
|
+
const path = join(projectRoot, name);
|
|
14
|
+
const originalContent = existingName ? readFileSync(path, "utf8") : undefined;
|
|
15
|
+
const original = originalContent ?? defaultConfiguration(framework);
|
|
16
|
+
const content = configureSource(original, framework, name);
|
|
17
|
+
if (content === original)
|
|
18
|
+
return undefined;
|
|
19
|
+
return { path, content, created: !existingName, originalContent };
|
|
20
|
+
}
|
|
21
|
+
/** Adds the selected adapter to a conventional host configuration source. */
|
|
22
|
+
export function configureSource(source, framework, fileName) {
|
|
23
|
+
const sourceFile = parseSource(source, fileName);
|
|
24
|
+
if (framework === "astro") {
|
|
25
|
+
return configureArrayHost(source, sourceFile, "@nudge-ui/astro", "nudgeUiAstro", "integrations", "nudgeUiAstro()");
|
|
26
|
+
}
|
|
27
|
+
if (framework === "vite-react") {
|
|
28
|
+
return configureArrayHost(source, sourceFile, "@nudge-ui/vite-react", "nudgeUi", "plugins", "...nudgeUi()");
|
|
29
|
+
}
|
|
30
|
+
return configureNextSource(source, sourceFile);
|
|
31
|
+
}
|
|
32
|
+
function configureArrayHost(source, sourceFile, packageName, importName, propertyName, expression) {
|
|
33
|
+
const bindings = importedBindings(sourceFile, packageName, importName);
|
|
34
|
+
const callableBindings = new Set([...bindings, importName]);
|
|
35
|
+
const configObject = findDefineConfigObject(sourceFile, propertyName);
|
|
36
|
+
const matchingProperties = configObject.properties.filter((property) => propertyText(property.name) === propertyName);
|
|
37
|
+
if (matchingProperties.length > 1) {
|
|
38
|
+
throw new Error(`Could not update ${propertyName}: the configuration contains duplicate ${propertyName} properties.`);
|
|
39
|
+
}
|
|
40
|
+
const property = matchingProperties[0];
|
|
41
|
+
let configurationEdit;
|
|
42
|
+
if (property) {
|
|
43
|
+
if (!ts.isPropertyAssignment(property) || !ts.isArrayLiteralExpression(property.initializer)) {
|
|
44
|
+
throw new Error(`Could not update ${propertyName}: expected ${propertyName} to be an array.`);
|
|
45
|
+
}
|
|
46
|
+
if (!containsCall(property.initializer, callableBindings)) {
|
|
47
|
+
configurationEdit = insertArrayElement(source, sourceFile, property, expression);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
configurationEdit = insertObjectProperty(source, sourceFile, configObject, propertyName, expression);
|
|
52
|
+
}
|
|
53
|
+
const edits = configurationEdit ? [configurationEdit] : [];
|
|
54
|
+
if (bindings.length === 0) {
|
|
55
|
+
edits.push(importEdit(sourceFile, `import { ${importName} } from "${packageName}";`, false));
|
|
56
|
+
}
|
|
57
|
+
return applyEdits(source, edits);
|
|
58
|
+
}
|
|
59
|
+
function configureNextSource(source, sourceFile) {
|
|
60
|
+
const target = findNextExport(sourceFile);
|
|
61
|
+
const bindings = target.commonJs
|
|
62
|
+
? requiredBindings(sourceFile, "@nudge-ui/nextjs", "withNudgeUi")
|
|
63
|
+
: importedBindings(sourceFile, "@nudge-ui/nextjs", "withNudgeUi");
|
|
64
|
+
const callableBindings = new Set([...bindings, "withNudgeUi"]);
|
|
65
|
+
const edits = [];
|
|
66
|
+
if (!containsCall(target.expression, callableBindings)) {
|
|
67
|
+
const start = target.expression.getStart(sourceFile);
|
|
68
|
+
const end = target.expression.getEnd();
|
|
69
|
+
edits.push({ start, end, text: `withNudgeUi(${source.slice(start, end)})` });
|
|
70
|
+
}
|
|
71
|
+
if (bindings.length === 0) {
|
|
72
|
+
const statement = target.commonJs
|
|
73
|
+
? 'const { withNudgeUi } = require("@nudge-ui/nextjs");'
|
|
74
|
+
: 'import { withNudgeUi } from "@nudge-ui/nextjs";';
|
|
75
|
+
edits.push(importEdit(sourceFile, statement, target.commonJs));
|
|
76
|
+
}
|
|
77
|
+
return applyEdits(source, edits);
|
|
78
|
+
}
|
|
79
|
+
function parseSource(source, fileName) {
|
|
80
|
+
const scriptKind = fileName.endsWith(".ts") || fileName.endsWith(".mts")
|
|
81
|
+
? ts.ScriptKind.TS
|
|
82
|
+
: ts.ScriptKind.JS;
|
|
83
|
+
return ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, scriptKind);
|
|
84
|
+
}
|
|
85
|
+
function findDefineConfigObject(sourceFile, propertyName) {
|
|
86
|
+
const candidates = [];
|
|
87
|
+
visit(sourceFile, (node) => {
|
|
88
|
+
if (ts.isCallExpression(node)
|
|
89
|
+
&& ts.isIdentifier(node.expression)
|
|
90
|
+
&& node.expression.text === "defineConfig"
|
|
91
|
+
&& node.arguments[0]
|
|
92
|
+
&& ts.isObjectLiteralExpression(node.arguments[0]))
|
|
93
|
+
candidates.push(node.arguments[0]);
|
|
94
|
+
});
|
|
95
|
+
if (candidates.length !== 1) {
|
|
96
|
+
throw new Error(`Could not update ${propertyName}: expected exactly one defineConfig({ ... }) call.`);
|
|
97
|
+
}
|
|
98
|
+
return candidates[0];
|
|
99
|
+
}
|
|
100
|
+
function findNextExport(sourceFile) {
|
|
101
|
+
const exports = [];
|
|
102
|
+
for (const statement of sourceFile.statements) {
|
|
103
|
+
if (ts.isExportAssignment(statement) && !statement.isExportEquals) {
|
|
104
|
+
exports.push({ expression: statement.expression, commonJs: false });
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!ts.isExpressionStatement(statement) || !ts.isBinaryExpression(statement.expression))
|
|
108
|
+
continue;
|
|
109
|
+
const assignment = statement.expression;
|
|
110
|
+
if (assignment.operatorToken.kind !== ts.SyntaxKind.EqualsToken || !isModuleExports(assignment.left))
|
|
111
|
+
continue;
|
|
112
|
+
exports.push({ expression: assignment.right, commonJs: true });
|
|
113
|
+
}
|
|
114
|
+
if (exports.length !== 1) {
|
|
115
|
+
throw new Error("Could not update the Next.js configuration: expected exactly one default or module.exports assignment.");
|
|
116
|
+
}
|
|
117
|
+
return exports[0];
|
|
118
|
+
}
|
|
119
|
+
function isModuleExports(expression) {
|
|
120
|
+
return ts.isPropertyAccessExpression(expression)
|
|
121
|
+
&& ts.isIdentifier(expression.expression)
|
|
122
|
+
&& expression.expression.text === "module"
|
|
123
|
+
&& expression.name.text === "exports";
|
|
124
|
+
}
|
|
125
|
+
function importedBindings(sourceFile, packageName, importName) {
|
|
126
|
+
const bindings = [];
|
|
127
|
+
for (const statement of sourceFile.statements) {
|
|
128
|
+
if (!ts.isImportDeclaration(statement)
|
|
129
|
+
|| !ts.isStringLiteral(statement.moduleSpecifier)
|
|
130
|
+
|| statement.moduleSpecifier.text !== packageName
|
|
131
|
+
|| !statement.importClause?.namedBindings
|
|
132
|
+
|| !ts.isNamedImports(statement.importClause.namedBindings))
|
|
133
|
+
continue;
|
|
134
|
+
for (const element of statement.importClause.namedBindings.elements) {
|
|
135
|
+
if ((element.propertyName?.text ?? element.name.text) === importName)
|
|
136
|
+
bindings.push(element.name.text);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return bindings;
|
|
140
|
+
}
|
|
141
|
+
function requiredBindings(sourceFile, packageName, importName) {
|
|
142
|
+
const bindings = [];
|
|
143
|
+
for (const statement of sourceFile.statements) {
|
|
144
|
+
if (!ts.isVariableStatement(statement))
|
|
145
|
+
continue;
|
|
146
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
147
|
+
if (!declaration.initializer || !isRequireCall(declaration.initializer, packageName))
|
|
148
|
+
continue;
|
|
149
|
+
if (!ts.isObjectBindingPattern(declaration.name))
|
|
150
|
+
continue;
|
|
151
|
+
for (const element of declaration.name.elements) {
|
|
152
|
+
const importedName = element.propertyName && ts.isIdentifier(element.propertyName)
|
|
153
|
+
? element.propertyName.text
|
|
154
|
+
: element.name.getText(sourceFile);
|
|
155
|
+
if (importedName === importName && ts.isIdentifier(element.name))
|
|
156
|
+
bindings.push(element.name.text);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return bindings;
|
|
161
|
+
}
|
|
162
|
+
function isRequireCall(expression, packageName) {
|
|
163
|
+
if (!ts.isCallExpression(expression) || expression.arguments.length !== 1)
|
|
164
|
+
return false;
|
|
165
|
+
const argument = expression.arguments[0];
|
|
166
|
+
if (!argument)
|
|
167
|
+
return false;
|
|
168
|
+
return ts.isCallExpression(expression)
|
|
169
|
+
&& ts.isIdentifier(expression.expression)
|
|
170
|
+
&& expression.expression.text === "require"
|
|
171
|
+
&& ts.isStringLiteral(argument)
|
|
172
|
+
&& argument.text === packageName;
|
|
173
|
+
}
|
|
174
|
+
function containsCall(root, bindingNames) {
|
|
175
|
+
let found = false;
|
|
176
|
+
visit(root, (node) => {
|
|
177
|
+
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression) && bindingNames.has(node.expression.text)) {
|
|
178
|
+
found = true;
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
return found;
|
|
182
|
+
}
|
|
183
|
+
function visit(root, visitor) {
|
|
184
|
+
visitor(root);
|
|
185
|
+
ts.forEachChild(root, (child) => visit(child, visitor));
|
|
186
|
+
}
|
|
187
|
+
function propertyText(name) {
|
|
188
|
+
if (!name)
|
|
189
|
+
return undefined;
|
|
190
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name))
|
|
191
|
+
return name.text;
|
|
192
|
+
if (ts.isComputedPropertyName(name) && ts.isStringLiteral(name.expression))
|
|
193
|
+
return name.expression.text;
|
|
194
|
+
return undefined;
|
|
195
|
+
}
|
|
196
|
+
function insertArrayElement(source, sourceFile, property, expression) {
|
|
197
|
+
const array = property.initializer;
|
|
198
|
+
if (!ts.isArrayLiteralExpression(array))
|
|
199
|
+
throw new Error("Expected an array property.");
|
|
200
|
+
const start = array.getStart(sourceFile) + 1;
|
|
201
|
+
const end = array.getEnd() - 1;
|
|
202
|
+
const indentation = lineIndentation(source, property.getStart(sourceFile));
|
|
203
|
+
const elementIndentation = `${indentation} `;
|
|
204
|
+
const interior = source.slice(start, end);
|
|
205
|
+
if (interior.trim() === "") {
|
|
206
|
+
return { start, end, text: `\n${elementIndentation}${expression},\n${indentation}` };
|
|
207
|
+
}
|
|
208
|
+
const text = interior.startsWith("\n")
|
|
209
|
+
? `\n${elementIndentation}${expression},`
|
|
210
|
+
: `\n${elementIndentation}${expression},\n${elementIndentation}`;
|
|
211
|
+
return { start, end: start, text };
|
|
212
|
+
}
|
|
213
|
+
function insertObjectProperty(source, sourceFile, object, propertyName, expression) {
|
|
214
|
+
const start = object.getStart(sourceFile) + 1;
|
|
215
|
+
const indentation = lineIndentation(source, object.getStart(sourceFile));
|
|
216
|
+
const propertyIndentation = `${indentation} `;
|
|
217
|
+
const empty = object.properties.length === 0;
|
|
218
|
+
const text = empty
|
|
219
|
+
? `\n${propertyIndentation}${propertyName}: [${expression}],\n${indentation}`
|
|
220
|
+
: `\n${propertyIndentation}${propertyName}: [${expression}],`;
|
|
221
|
+
return { start, end: start, text };
|
|
222
|
+
}
|
|
223
|
+
function importEdit(sourceFile, statement, commonJs) {
|
|
224
|
+
const imports = sourceFile.statements.filter(ts.isImportDeclaration);
|
|
225
|
+
const lastImport = imports.at(-1);
|
|
226
|
+
if (!commonJs && lastImport)
|
|
227
|
+
return { start: lastImport.end, end: lastImport.end, text: `\n${statement}` };
|
|
228
|
+
if (commonJs) {
|
|
229
|
+
const directives = sourceFile.statements.filter((candidate) => ts.isExpressionStatement(candidate) && ts.isStringLiteral(candidate.expression));
|
|
230
|
+
const lastDirective = directives.at(-1);
|
|
231
|
+
if (lastDirective)
|
|
232
|
+
return { start: lastDirective.end, end: lastDirective.end, text: `\n${statement}` };
|
|
233
|
+
}
|
|
234
|
+
const firstStatement = sourceFile.statements[0];
|
|
235
|
+
const start = firstStatement?.getStart(sourceFile) ?? sourceFile.getEnd();
|
|
236
|
+
return { start, end: start, text: `${statement}\n` };
|
|
237
|
+
}
|
|
238
|
+
function lineIndentation(source, position) {
|
|
239
|
+
const lineStart = source.lastIndexOf("\n", position - 1) + 1;
|
|
240
|
+
const match = /^[ \t]*/.exec(source.slice(lineStart, position));
|
|
241
|
+
return match?.[0] ?? "";
|
|
242
|
+
}
|
|
243
|
+
function applyEdits(source, edits) {
|
|
244
|
+
return [...edits]
|
|
245
|
+
.sort((left, right) => right.start - left.start)
|
|
246
|
+
.reduce((result, edit) => `${result.slice(0, edit.start)}${edit.text}${result.slice(edit.end)}`, source);
|
|
247
|
+
}
|
|
248
|
+
function configurationNames(framework) {
|
|
249
|
+
if (framework === "nextjs")
|
|
250
|
+
return nextConfigNames;
|
|
251
|
+
if (framework === "astro")
|
|
252
|
+
return astroConfigNames;
|
|
253
|
+
return viteConfigNames;
|
|
254
|
+
}
|
|
255
|
+
function defaultConfigName(framework) {
|
|
256
|
+
if (framework === "nextjs")
|
|
257
|
+
return "next.config.mjs";
|
|
258
|
+
if (framework === "astro")
|
|
259
|
+
return "astro.config.mjs";
|
|
260
|
+
return "vite.config.mjs";
|
|
261
|
+
}
|
|
262
|
+
function defaultConfiguration(framework) {
|
|
263
|
+
if (framework === "nextjs")
|
|
264
|
+
return "const nextConfig = {};\n\nexport default nextConfig;\n";
|
|
265
|
+
if (framework === "astro") {
|
|
266
|
+
return 'import { defineConfig } from "astro/config";\n\nexport default defineConfig({});\n';
|
|
267
|
+
}
|
|
268
|
+
return 'import { defineConfig } from "vite";\n\nexport default defineConfig({});\n';
|
|
269
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Framework, PackageManager, ProjectManifest } from "./types.js";
|
|
2
|
+
/** Reads only the package metadata needed for framework and package-manager detection. */
|
|
3
|
+
export declare function readProjectManifest(projectRoot: string): ProjectManifest;
|
|
4
|
+
/** Detects viable host adapters without treating React or Vite as host frameworks. */
|
|
5
|
+
export declare function detectFrameworks(projectRoot: string, manifest?: ProjectManifest): Framework[];
|
|
6
|
+
/** Detects the project's package manager, preferring its declared package manager. */
|
|
7
|
+
export declare function detectPackageManager(projectRoot: string, manifest?: ProjectManifest): PackageManager;
|
|
8
|
+
/** Finds an HTML document root without recursively scanning dependency or build output. */
|
|
9
|
+
export declare function detectStaticRoot(projectRoot: string): string | undefined;
|
|
10
|
+
export declare function frameworkDisplayName(framework: Framework): string;
|
|
11
|
+
export declare function adapterPackage(framework: Framework): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
const nextConfigNames = ["next.config.ts", "next.config.mts", "next.config.mjs", "next.config.js", "next.config.cjs"];
|
|
4
|
+
const astroConfigNames = ["astro.config.ts", "astro.config.mts", "astro.config.mjs", "astro.config.js"];
|
|
5
|
+
const viteConfigNames = ["vite.config.ts", "vite.config.mts", "vite.config.mjs", "vite.config.js"];
|
|
6
|
+
/** Reads only the package metadata needed for framework and package-manager detection. */
|
|
7
|
+
export function readProjectManifest(projectRoot) {
|
|
8
|
+
const manifestPath = join(projectRoot, "package.json");
|
|
9
|
+
if (!existsSync(manifestPath)) {
|
|
10
|
+
return { dependencies: new Set() };
|
|
11
|
+
}
|
|
12
|
+
let source;
|
|
13
|
+
try {
|
|
14
|
+
source = readFileSync(manifestPath, "utf8");
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
18
|
+
throw new Error(`Could not read ${manifestPath}: ${message}`, { cause: error });
|
|
19
|
+
}
|
|
20
|
+
let parsed;
|
|
21
|
+
try {
|
|
22
|
+
// SAFETY: The object shape is validated before any property is consumed.
|
|
23
|
+
parsed = JSON.parse(source);
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
27
|
+
throw new Error(`Could not parse ${manifestPath}: ${message}`, { cause: error });
|
|
28
|
+
}
|
|
29
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
30
|
+
throw new Error(`${manifestPath} must contain a JSON object.`);
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
type: stringProperty(parsed, "type"),
|
|
34
|
+
packageManager: stringProperty(parsed, "packageManager"),
|
|
35
|
+
dependencies: new Set([
|
|
36
|
+
...dependencyNames(parsed, "dependencies"),
|
|
37
|
+
...dependencyNames(parsed, "devDependencies"),
|
|
38
|
+
...dependencyNames(parsed, "peerDependencies"),
|
|
39
|
+
...dependencyNames(parsed, "optionalDependencies"),
|
|
40
|
+
]),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Detects viable host adapters without treating React or Vite as host frameworks. */
|
|
44
|
+
export function detectFrameworks(projectRoot, manifest = readProjectManifest(projectRoot)) {
|
|
45
|
+
const hasNext = manifest.dependencies.has("next") || hasAnyFile(projectRoot, nextConfigNames);
|
|
46
|
+
const hasAstro = manifest.dependencies.has("astro") || hasAnyFile(projectRoot, astroConfigNames);
|
|
47
|
+
const hosts = [];
|
|
48
|
+
if (hasNext)
|
|
49
|
+
hosts.push("nextjs");
|
|
50
|
+
if (hasAstro)
|
|
51
|
+
hosts.push("astro");
|
|
52
|
+
if (hosts.length > 0)
|
|
53
|
+
return hosts;
|
|
54
|
+
const hasVite = manifest.dependencies.has("vite") || hasAnyFile(projectRoot, viteConfigNames);
|
|
55
|
+
const hasReact = manifest.dependencies.has("react") || manifest.dependencies.has("@vitejs/plugin-react");
|
|
56
|
+
if (hasVite && hasReact)
|
|
57
|
+
return ["vite-react"];
|
|
58
|
+
if (hasVite)
|
|
59
|
+
return [];
|
|
60
|
+
if (manifest.dependencies.has("@nudge-ui/standalone") || detectStaticRoot(projectRoot))
|
|
61
|
+
return ["standalone"];
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
/** Detects the project's package manager, preferring its declared package manager. */
|
|
65
|
+
export function detectPackageManager(projectRoot, manifest = readProjectManifest(projectRoot)) {
|
|
66
|
+
const declared = manifest.packageManager?.split("@")[0];
|
|
67
|
+
if (declared === "pnpm" || declared === "npm" || declared === "yarn" || declared === "bun")
|
|
68
|
+
return declared;
|
|
69
|
+
let directory = projectRoot;
|
|
70
|
+
while (true) {
|
|
71
|
+
if (existsSync(join(directory, "pnpm-lock.yaml")))
|
|
72
|
+
return "pnpm";
|
|
73
|
+
if (existsSync(join(directory, "yarn.lock")))
|
|
74
|
+
return "yarn";
|
|
75
|
+
if (existsSync(join(directory, "bun.lock")) || existsSync(join(directory, "bun.lockb")))
|
|
76
|
+
return "bun";
|
|
77
|
+
if (existsSync(join(directory, "package-lock.json")))
|
|
78
|
+
return "npm";
|
|
79
|
+
const parent = dirname(directory);
|
|
80
|
+
if (parent === directory)
|
|
81
|
+
break;
|
|
82
|
+
directory = parent;
|
|
83
|
+
}
|
|
84
|
+
return "npm";
|
|
85
|
+
}
|
|
86
|
+
/** Finds an HTML document root without recursively scanning dependency or build output. */
|
|
87
|
+
export function detectStaticRoot(projectRoot) {
|
|
88
|
+
const entries = readDirectory(projectRoot);
|
|
89
|
+
if (entries.some((entry) => entry.isFile() && entry.name.endsWith(".html")))
|
|
90
|
+
return projectRoot;
|
|
91
|
+
for (const entry of entries) {
|
|
92
|
+
if (!entry.isDirectory() || entry.name.startsWith(".") || ignoredStaticDirectories.has(entry.name))
|
|
93
|
+
continue;
|
|
94
|
+
const directory = join(projectRoot, entry.name);
|
|
95
|
+
const children = readDirectory(directory);
|
|
96
|
+
if (children.some((child) => child.isFile() && child.name.endsWith(".html")))
|
|
97
|
+
return directory;
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
export function frameworkDisplayName(framework) {
|
|
102
|
+
if (framework === "nextjs")
|
|
103
|
+
return "Next.js";
|
|
104
|
+
if (framework === "astro")
|
|
105
|
+
return "Astro";
|
|
106
|
+
if (framework === "vite-react")
|
|
107
|
+
return "Vite with React";
|
|
108
|
+
return "static HTML";
|
|
109
|
+
}
|
|
110
|
+
export function adapterPackage(framework) {
|
|
111
|
+
if (framework === "nextjs")
|
|
112
|
+
return "@nudge-ui/nextjs";
|
|
113
|
+
if (framework === "astro")
|
|
114
|
+
return "@nudge-ui/astro";
|
|
115
|
+
if (framework === "vite-react")
|
|
116
|
+
return "@nudge-ui/vite-react";
|
|
117
|
+
return "@nudge-ui/standalone";
|
|
118
|
+
}
|
|
119
|
+
function hasAnyFile(projectRoot, names) {
|
|
120
|
+
return names.some((name) => existsSync(join(projectRoot, name)));
|
|
121
|
+
}
|
|
122
|
+
const ignoredStaticDirectories = new Set(["node_modules", "dist", "build", "coverage", "test-results"]);
|
|
123
|
+
function stringProperty(value, key) {
|
|
124
|
+
const property = key === "type" ? value.type : value.packageManager;
|
|
125
|
+
return typeof property === "string" ? property : undefined;
|
|
126
|
+
}
|
|
127
|
+
function dependencyNames(value, key) {
|
|
128
|
+
const dependencies = value[key];
|
|
129
|
+
if (typeof dependencies !== "object" || dependencies === null || Array.isArray(dependencies))
|
|
130
|
+
return [];
|
|
131
|
+
return Object.keys(dependencies);
|
|
132
|
+
}
|
|
133
|
+
function readDirectory(path) {
|
|
134
|
+
try {
|
|
135
|
+
return readdirSync(path, { withFileTypes: true });
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
139
|
+
throw new Error(`Could not inspect ${path}: ${message}`, { cause: error });
|
|
140
|
+
}
|
|
141
|
+
}
|
package/dist/entry.d.ts
ADDED
package/dist/entry.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseArguments, helpText, type CliOptions } from "./arguments.js";
|
|
2
|
+
export { configureSource, planConfiguration } from "./configuration.js";
|
|
3
|
+
export { adapterPackage, detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
|
|
4
|
+
export { formatCommand, installCommand, type InstallCommand } from "./installer.js";
|
|
5
|
+
export { frameworks, packageManagers, type Framework, type PackageManager } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { parseArguments, helpText } from "./arguments.js";
|
|
2
|
+
export { configureSource, planConfiguration } from "./configuration.js";
|
|
3
|
+
export { adapterPackage, detectFrameworks, detectPackageManager, detectStaticRoot, frameworkDisplayName, readProjectManifest, } from "./detection.js";
|
|
4
|
+
export { formatCommand, installCommand } from "./installer.js";
|
|
5
|
+
export { frameworks, packageManagers } from "./types.js";
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PackageManager } from "./types.js";
|
|
2
|
+
export interface InstallCommand {
|
|
3
|
+
readonly executable: string;
|
|
4
|
+
readonly args: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
/** Returns the native development-dependency command for a package manager. */
|
|
7
|
+
export declare function installCommand(packageManager: PackageManager, packageName: string): InstallCommand;
|
|
8
|
+
/** Runs a package-manager command with the user's terminal attached. */
|
|
9
|
+
export declare function installPackage(command: InstallCommand, projectRoot: string): void;
|
|
10
|
+
export declare function formatCommand(command: InstallCommand): string;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
/** Returns the native development-dependency command for a package manager. */
|
|
3
|
+
export function installCommand(packageManager, packageName) {
|
|
4
|
+
if (packageManager === "pnpm")
|
|
5
|
+
return { executable: "pnpm", args: ["add", "-D", packageName] };
|
|
6
|
+
if (packageManager === "yarn")
|
|
7
|
+
return { executable: "yarn", args: ["add", "-D", packageName] };
|
|
8
|
+
if (packageManager === "bun")
|
|
9
|
+
return { executable: "bun", args: ["add", "--dev", packageName] };
|
|
10
|
+
return { executable: "npm", args: ["install", "--save-dev", packageName] };
|
|
11
|
+
}
|
|
12
|
+
/** Runs a package-manager command with the user's terminal attached. */
|
|
13
|
+
export function installPackage(command, projectRoot) {
|
|
14
|
+
const executable = process.platform === "win32" ? `${command.executable}.cmd` : command.executable;
|
|
15
|
+
const result = spawnSync(executable, command.args, { cwd: projectRoot, stdio: "inherit" });
|
|
16
|
+
if (result.error)
|
|
17
|
+
throw result.error;
|
|
18
|
+
if (result.status !== 0) {
|
|
19
|
+
throw new Error(`${command.executable} exited with status ${result.status ?? "unknown"}.`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export function formatCommand(command) {
|
|
23
|
+
return [command.executable, ...command.args].join(" ");
|
|
24
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export declare const frameworks: readonly ["nextjs", "astro", "vite-react", "standalone"];
|
|
2
|
+
export type Framework = typeof frameworks[number];
|
|
3
|
+
export declare const packageManagers: readonly ["pnpm", "npm", "yarn", "bun"];
|
|
4
|
+
export type PackageManager = typeof packageManagers[number];
|
|
5
|
+
export interface ProjectManifest {
|
|
6
|
+
readonly type?: string;
|
|
7
|
+
readonly packageManager?: string;
|
|
8
|
+
readonly dependencies: ReadonlySet<string>;
|
|
9
|
+
}
|
|
10
|
+
export interface ConfigurationChange {
|
|
11
|
+
readonly path: string;
|
|
12
|
+
readonly content: string;
|
|
13
|
+
readonly created: boolean;
|
|
14
|
+
readonly originalContent?: string;
|
|
15
|
+
}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-nudge-ui",
|
|
3
|
+
"version": "0.1.2",
|
|
4
|
+
"description": "Framework-detecting installer for Nudge UI.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/charlessmart/nudge-ui.git",
|
|
9
|
+
"directory": "packages/create-nudge-ui"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/charlessmart/nudge-ui/issues"
|
|
13
|
+
},
|
|
14
|
+
"homepage": "https://github.com/charlessmart/nudge-ui#readme",
|
|
15
|
+
"type": "module",
|
|
16
|
+
"files": [
|
|
17
|
+
"dist"
|
|
18
|
+
],
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"types": "./dist/index.d.ts",
|
|
24
|
+
"import": "./dist/index.js",
|
|
25
|
+
"default": "./dist/index.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"bin": {
|
|
29
|
+
"create-nudge-ui": "./dist/entry.js"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"vitest": "^2.1.9"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"publishConfig": {
|
|
41
|
+
"access": "public"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "node ../../scripts/build-package.mjs",
|
|
45
|
+
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
46
|
+
"test:unit": "vitest run",
|
|
47
|
+
"test:unit:watch": "vitest"
|
|
48
|
+
}
|
|
49
|
+
}
|