@abcnews/aunty 17.0.0-next.3 → 17.0.0-next.5
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/.prettierrc +1 -0
- package/.vscode/settings.json +5 -0
- package/CONTRIBUTING.md +39 -0
- package/README.md +23 -0
- package/TEMPLATES.md +112 -0
- package/assets/done.mp3 +0 -0
- package/eslint.config.mjs +12 -0
- package/package.json +25 -13
- package/scripts/postinstall.ts +7 -0
- package/src/bin/aunty.js +72 -0
- package/src/bin/commander.ts +174 -0
- package/src/commands/build/index.ts +23 -0
- package/src/commands/create/index.ts +148 -0
- package/src/commands/create/types.ts +7 -0
- package/src/commands/deploy/fs.ts +32 -0
- package/src/commands/deploy/index.ts +155 -0
- package/src/commands/migrate/index.ts +320 -0
- package/src/commands/release/index.ts +127 -0
- package/src/commands/release-check/git.ts +63 -0
- package/src/commands/release-check/index.ts +165 -0
- package/src/commands/serve/index.ts +44 -0
- package/src/lib/colours.json +12 -0
- package/src/lib/constants.ts +4 -0
- package/src/lib/ftp.ts +167 -0
- package/src/lib/git.ts +93 -0
- package/src/lib/initHelpers.ts +208 -0
- package/src/lib/semver.ts +50 -0
- package/src/lib/terminal.ts +130 -0
- package/src/lib/util.ts +100 -0
- package/src/lib/vite.ts +179 -0
- package/src/types.ts +16 -0
- package/templates/index.ts +11 -0
- package/templates/svelte/base/README.md +26 -0
- package/templates/svelte/base/contents/.editorconfig +12 -0
- package/templates/svelte/base/contents/.prettierrc +7 -0
- package/templates/svelte/base/contents/.vscode/extensions.json +3 -0
- package/templates/svelte/base/contents/LICENSE +21 -0
- package/templates/svelte/base/contents/README.md +16 -0
- package/templates/svelte/base/contents/_gitignore +24 -0
- package/templates/svelte/base/contents/index.html +22 -0
- package/templates/svelte/base/contents/package-lock.json +1373 -0
- package/templates/svelte/base/contents/package.json +29 -0
- package/templates/svelte/base/contents/src/App.svelte +21 -0
- package/templates/svelte/base/contents/src/assets/favicon.svg +4 -0
- package/templates/svelte/base/contents/src/components/Worm/Worm.svelte +59 -0
- package/templates/svelte/base/contents/src/components/Worm/worm.svg +4 -0
- package/templates/svelte/base/contents/src/iframe.ts +15 -0
- package/templates/svelte/base/contents/src/index.ts +26 -0
- package/templates/svelte/base/contents/svelte.config.js +2 -0
- package/templates/svelte/base/contents/tsconfig.app.json +22 -0
- package/templates/svelte/base/contents/tsconfig.json +7 -0
- package/templates/svelte/base/contents/tsconfig.node.json +28 -0
- package/templates/svelte/base/contents/vite-env.d.ts +7 -0
- package/templates/svelte/base/contents/vite.config.ts +40 -0
- package/templates/svelte/base/init.ts +39 -0
- package/templates/svelte/index.ts +52 -0
- package/templates/svelte/patch-builder/contents/builder/index.html +39 -0
- package/templates/svelte/patch-builder/contents/src/components/Builder/Builder.svelte +15 -0
- package/templates/svelte/patch-builder/init.ts +43 -0
- package/templates/svelte/patch-js/README.md +16 -0
- package/templates/svelte/patch-js/init.ts +61 -0
- package/templates/svelte/patch-odyssey/init.ts +10 -0
- package/templates/svelte/patch-scrollyteller/README.md +6 -0
- package/templates/svelte/patch-scrollyteller/contents/index.html +35 -0
- package/templates/svelte/patch-scrollyteller/contents/src/App.svelte +37 -0
- package/templates/svelte/patch-scrollyteller/contents/src/index.ts +36 -0
- package/templates/svelte/patch-scrollyteller/contents/src/types.ts +2 -0
- package/templates/svelte/patch-scrollyteller/init.ts +20 -0
- package/templates/vanilla/base/contents/LICENSE +21 -0
- package/templates/vanilla/base/contents/_gitignore +24 -0
- package/templates/vanilla/base/contents/index.html +22 -0
- package/templates/vanilla/base/contents/package-lock.json +999 -0
- package/templates/vanilla/base/contents/package.json +25 -0
- package/templates/vanilla/base/contents/public/favicon.svg +1 -0
- package/templates/vanilla/base/contents/public/icons.svg +24 -0
- package/templates/vanilla/base/contents/src/assets/hero.png +0 -0
- package/templates/vanilla/base/contents/src/assets/javascript.svg +1 -0
- package/templates/vanilla/base/contents/src/assets/vite.svg +1 -0
- package/templates/vanilla/base/contents/src/iframe.ts +17 -0
- package/templates/vanilla/base/contents/src/index.ts +17 -0
- package/templates/vanilla/base/contents/src/style.css +8 -0
- package/templates/vanilla/base/contents/tsconfig.app.json +23 -0
- package/templates/vanilla/base/contents/tsconfig.json +7 -0
- package/templates/vanilla/base/contents/tsconfig.node.json +28 -0
- package/templates/vanilla/base/contents/vite-env.d.ts +7 -0
- package/templates/vanilla/base/contents/vite.config.js +38 -0
- package/templates/vanilla/base/init.ts +33 -0
- package/templates/vanilla/index.ts +34 -0
- package/templates/vanilla/patch-js/README.md +0 -0
- package/templates/vanilla/patch-js/init.ts +27 -0
- package/test/helpers.ts +198 -0
- package/test/semver.test.ts +45 -0
- package/test/templates-e2e.test.ts +268 -0
- package/tsconfig.json +18 -0
- package/src/bin/aunty.ts +0 -60
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import {
|
|
2
|
+
intro,
|
|
3
|
+
outro,
|
|
4
|
+
text,
|
|
5
|
+
select,
|
|
6
|
+
confirm,
|
|
7
|
+
log,
|
|
8
|
+
cancel,
|
|
9
|
+
isCancel,
|
|
10
|
+
} from "@clack/prompts";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import fs from "node:fs/promises";
|
|
13
|
+
import pc from "picocolors";
|
|
14
|
+
import { $ } from "zx";
|
|
15
|
+
import { getHeader, spin } from "../../lib/terminal.ts";
|
|
16
|
+
import { isProjectNameAndVersionAvailable } from "../../lib/ftp.ts";
|
|
17
|
+
import templates from "../../../templates/index.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The main entry point for the 'aunty create' command.
|
|
21
|
+
*/
|
|
22
|
+
export async function run(destDirArg?: string): Promise<number> {
|
|
23
|
+
intro(getHeader(pc.dim("aunty"), "create", { colour: "green" }));
|
|
24
|
+
|
|
25
|
+
// 1. Get project name
|
|
26
|
+
const projectName =
|
|
27
|
+
destDirArg ||
|
|
28
|
+
(await text({
|
|
29
|
+
message: "What is your project named?",
|
|
30
|
+
placeholder: destDirArg || "my-new-project",
|
|
31
|
+
validate: (value) => {
|
|
32
|
+
if (!value || value.length === 0) {
|
|
33
|
+
return "Project name is required";
|
|
34
|
+
}
|
|
35
|
+
if (!value.match(/^[a-z0-9-]+$/)) {
|
|
36
|
+
return "Project name should be lowercase-alphanumeric-with-hypens";
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
}));
|
|
40
|
+
|
|
41
|
+
if (isCancel(projectName)) {
|
|
42
|
+
cancel("Operation cancelled.");
|
|
43
|
+
return 0;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const finalProjectName = projectName || destDirArg || "my-new-project";
|
|
47
|
+
const projectDest = path.resolve(process.cwd(), finalProjectName);
|
|
48
|
+
|
|
49
|
+
// 1.5. Check FTP for name collision
|
|
50
|
+
const ftpSpinner = spin("Checking project name availability...");
|
|
51
|
+
const isAvailable = await isProjectNameAndVersionAvailable(finalProjectName);
|
|
52
|
+
ftpSpinner.stop("Project name checked");
|
|
53
|
+
|
|
54
|
+
if (isAvailable === "error") {
|
|
55
|
+
const shouldContinue = await confirm({
|
|
56
|
+
message: pc.red(
|
|
57
|
+
"Could not connect to FTP to check name availability. Continue anyway?",
|
|
58
|
+
),
|
|
59
|
+
initialValue: true,
|
|
60
|
+
});
|
|
61
|
+
if (!shouldContinue || isCancel(shouldContinue)) {
|
|
62
|
+
cancel("Create cancelled.");
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (isAvailable === "exists") {
|
|
67
|
+
const shouldContinue = await confirm({
|
|
68
|
+
message: pc.yellow(
|
|
69
|
+
`Project name "${finalProjectName}" already exists on FTP. Continue?`,
|
|
70
|
+
),
|
|
71
|
+
initialValue: false,
|
|
72
|
+
});
|
|
73
|
+
if (!shouldContinue || isCancel(shouldContinue)) {
|
|
74
|
+
cancel("Create cancelled.");
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 2. Select project type
|
|
80
|
+
const projectType = await select({
|
|
81
|
+
message: "Select a project type:",
|
|
82
|
+
options: templates.projects.map((p) => ({ value: p, label: p.name })),
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (isCancel(projectType)) {
|
|
86
|
+
cancel("Operation cancelled.");
|
|
87
|
+
return 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const selectedProject = projectType;
|
|
91
|
+
|
|
92
|
+
// 3. Create directory
|
|
93
|
+
if (
|
|
94
|
+
await fs
|
|
95
|
+
.access(projectDest)
|
|
96
|
+
.then(() => true)
|
|
97
|
+
.catch(() => false)
|
|
98
|
+
) {
|
|
99
|
+
cancel(`Directory ${pc.cyan(projectDest)} already exists.`);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 4. Run initialization
|
|
104
|
+
try {
|
|
105
|
+
const exitCode = await selectedProject.run({
|
|
106
|
+
projectName: finalProjectName,
|
|
107
|
+
baseDir: projectDest,
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (exitCode !== 0) {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
} catch (err: unknown) {
|
|
114
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
115
|
+
cancel(`Failed to create project: ${message}`);
|
|
116
|
+
if (err instanceof Error && err.stack) {
|
|
117
|
+
console.error(err.stack);
|
|
118
|
+
}
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// 7. NPM Install
|
|
123
|
+
const installSpinner = spin("Installing dependencies...");
|
|
124
|
+
try {
|
|
125
|
+
await $({ cwd: projectDest })`npm install`.quiet();
|
|
126
|
+
installSpinner.stop("Dependencies installed");
|
|
127
|
+
} catch {
|
|
128
|
+
installSpinner.cancel(
|
|
129
|
+
"Failed to install dependencies. You may need to run 'npm install' manually.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 8. Initialize Git Repository
|
|
134
|
+
try {
|
|
135
|
+
await $({ cwd: projectDest })`git init`.quiet();
|
|
136
|
+
} catch {}
|
|
137
|
+
|
|
138
|
+
log.step("Next steps:");
|
|
139
|
+
log.message(
|
|
140
|
+
`
|
|
141
|
+
${pc.bold(1)}. cd ${finalProjectName}
|
|
142
|
+
${pc.bold(2)}. npm run dev
|
|
143
|
+
`.trim(),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
outro("Happy coding.");
|
|
147
|
+
return 0;
|
|
148
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Recursively gets a list of all files in a directory.
|
|
6
|
+
* @param dirPath Path to the directory
|
|
7
|
+
* @param baseDir Optional base directory to calculate relative paths from
|
|
8
|
+
* @returns Array of file info objects
|
|
9
|
+
*/
|
|
10
|
+
export async function getFileInventory(
|
|
11
|
+
dirPath: string,
|
|
12
|
+
baseDir: string = dirPath,
|
|
13
|
+
): Promise<{ path: string; relPath: string; size: number }[]> {
|
|
14
|
+
const inventory: { path: string; relPath: string; size: number }[] = [];
|
|
15
|
+
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
16
|
+
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
19
|
+
if (entry.isDirectory()) {
|
|
20
|
+
inventory.push(...(await getFileInventory(fullPath, baseDir)));
|
|
21
|
+
} else if (entry.isFile()) {
|
|
22
|
+
const stats = await fs.stat(fullPath);
|
|
23
|
+
inventory.push({
|
|
24
|
+
path: fullPath,
|
|
25
|
+
relPath: path.relative(baseDir, fullPath),
|
|
26
|
+
size: stats.size,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return inventory;
|
|
32
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { intro, outro, confirm, log, cancel, isCancel } from "@clack/prompts";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import pc from "picocolors";
|
|
4
|
+
import { FtpClient } from "../../lib/ftp.ts";
|
|
5
|
+
import { formatSize, findProjectDetails } from "../../lib/util.ts";
|
|
6
|
+
import { getHeader, spin } from "../../lib/terminal.ts";
|
|
7
|
+
import {
|
|
8
|
+
BUILD_DIRECTORY_NAME,
|
|
9
|
+
FTP_PROJECTS_PATH,
|
|
10
|
+
PUBLIC_PROJECTS_URL,
|
|
11
|
+
} from "../../lib/constants.ts";
|
|
12
|
+
import { getFileInventory } from "./fs.ts";
|
|
13
|
+
import slugify from "slugify";
|
|
14
|
+
|
|
15
|
+
interface DeployOptions {
|
|
16
|
+
destDir?: string;
|
|
17
|
+
buildDir?: string;
|
|
18
|
+
dryRun?: boolean;
|
|
19
|
+
force?: boolean;
|
|
20
|
+
skipHeader?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The main entry point for the 'aunty deploy' command.
|
|
25
|
+
*/
|
|
26
|
+
export async function run(options: DeployOptions = {}): Promise<number> {
|
|
27
|
+
if (!options.skipHeader) {
|
|
28
|
+
intro(
|
|
29
|
+
getHeader(
|
|
30
|
+
pc.dim("aunty"),
|
|
31
|
+
`deploy${options.dryRun ? ` ${pc.cyan("[dry]")}` : ""}`,
|
|
32
|
+
{ colour: "red" },
|
|
33
|
+
),
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 1. Load config
|
|
38
|
+
const details = await findProjectDetails(process.cwd());
|
|
39
|
+
|
|
40
|
+
if (!details) return 1;
|
|
41
|
+
|
|
42
|
+
const { root: projectRoot, pkg: config } = details;
|
|
43
|
+
const { name, version } = config;
|
|
44
|
+
if (!name || !version) {
|
|
45
|
+
cancel("Missing name or version in package.json");
|
|
46
|
+
return 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 3. Construct target path
|
|
50
|
+
const localDir = path.resolve(
|
|
51
|
+
projectRoot,
|
|
52
|
+
options.buildDir || BUILD_DIRECTORY_NAME,
|
|
53
|
+
);
|
|
54
|
+
const nameSlug = (slugify as unknown as (s: string, o: object) => string)(
|
|
55
|
+
name,
|
|
56
|
+
{ strict: true },
|
|
57
|
+
);
|
|
58
|
+
const targetFolder = options.destDir || version;
|
|
59
|
+
const remoteDir = path.join(FTP_PROJECTS_PATH, nameSlug, targetFolder, "/");
|
|
60
|
+
const publicUrl = `${PUBLIC_PROJECTS_URL}${nameSlug}/${targetFolder}/`;
|
|
61
|
+
|
|
62
|
+
// 4. File Inventory & Size Check
|
|
63
|
+
let inventory;
|
|
64
|
+
try {
|
|
65
|
+
inventory = await getFileInventory(localDir);
|
|
66
|
+
} catch {
|
|
67
|
+
cancel(
|
|
68
|
+
`Build directory not found at ${pc.cyan(localDir)}. Have you run the build command?`,
|
|
69
|
+
);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (inventory.length === 0) {
|
|
74
|
+
cancel(`Build directory is empty! Nothing to deploy.`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const list = inventory
|
|
79
|
+
.map(
|
|
80
|
+
(f: { relPath: string; size: number }) =>
|
|
81
|
+
` ${pc.dim(f.relPath)} (${formatSize(f.size)})`,
|
|
82
|
+
)
|
|
83
|
+
.join("\n");
|
|
84
|
+
log.step(`Found ${pc.bold(inventory.length)} files to deploy:\n${list}`);
|
|
85
|
+
|
|
86
|
+
if (options.dryRun) {
|
|
87
|
+
outro(pc.green("Dry run complete. No files were uploaded."));
|
|
88
|
+
return 0;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 5. Credential Test & Confirmation
|
|
92
|
+
const ftpClient = new FtpClient();
|
|
93
|
+
try {
|
|
94
|
+
await ftpClient.connect();
|
|
95
|
+
} catch (err) {
|
|
96
|
+
log.error(
|
|
97
|
+
`FTP connection failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
98
|
+
);
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const exists = await ftpClient.exists(remoteDir);
|
|
103
|
+
|
|
104
|
+
if (exists && !options.force) {
|
|
105
|
+
// Close the connection before waiting on user input to avoid a socket timeout
|
|
106
|
+
ftpClient.close();
|
|
107
|
+
|
|
108
|
+
const shouldOverwrite = await confirm({
|
|
109
|
+
message: `${pc.red(`Directory ${pc.bold(remoteDir)} already exists.`)} Overwrite?`,
|
|
110
|
+
initialValue: false,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!shouldOverwrite || isCancel(shouldOverwrite)) {
|
|
114
|
+
cancel("Deploy cancelled.");
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Reconnect after the prompt
|
|
119
|
+
await ftpClient.connect();
|
|
120
|
+
} else if (!exists) {
|
|
121
|
+
await ftpClient.ensureDir(remoteDir);
|
|
122
|
+
} else if (options.force) {
|
|
123
|
+
log.info(pc.yellow("Force flag used. Overwriting remote directory."));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const uploadSpinner = spin("Uploading files...");
|
|
127
|
+
|
|
128
|
+
let uploadedCount = 0;
|
|
129
|
+
let currentFile = "";
|
|
130
|
+
const totalFilesStr = inventory.length.toString();
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
await ftpClient.uploadDir(localDir, remoteDir, (info) => {
|
|
134
|
+
if (info.name !== currentFile) {
|
|
135
|
+
uploadedCount++;
|
|
136
|
+
currentFile = info.name;
|
|
137
|
+
const countStr = uploadedCount
|
|
138
|
+
.toString()
|
|
139
|
+
.padStart(totalFilesStr.length, " ");
|
|
140
|
+
uploadSpinner.message(`${countStr}/${totalFilesStr} ${info.name}`);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
uploadSpinner.stop(`Upload finished`);
|
|
145
|
+
} catch (err) {
|
|
146
|
+
uploadSpinner.cancel("Upload failed");
|
|
147
|
+
ftpClient.close();
|
|
148
|
+
throw err;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
ftpClient.close();
|
|
152
|
+
|
|
153
|
+
outro(`${pc.bold("Released to:")} ${pc.cyan(publicUrl)}`);
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file
|
|
3
|
+
* A command to migrate an old Aunty Webpack project to Aunty Vite.
|
|
4
|
+
*
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { intro, outro, confirm, log, cancel, isCancel } from "@clack/prompts";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import fs from "node:fs/promises";
|
|
10
|
+
import pc from "picocolors";
|
|
11
|
+
import { $, glob } from "zx";
|
|
12
|
+
import { getHeader, spin } from "../../lib/terminal.ts";
|
|
13
|
+
import { findProjectDetails, loadJson } from "../../lib/util.ts";
|
|
14
|
+
import * as git from "../../lib/git.ts";
|
|
15
|
+
import { installAunty } from "../../lib/initHelpers.ts";
|
|
16
|
+
|
|
17
|
+
interface MigrateOptions {
|
|
18
|
+
yes?: boolean;
|
|
19
|
+
skipGit?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Migrates a legacy HTML file by removing legacy scripts and comments,
|
|
24
|
+
* injecting the new Vite entry script, writing it to the destination, and deleting the source.
|
|
25
|
+
*/
|
|
26
|
+
async function migrateHtmlFile(
|
|
27
|
+
srcPath: string,
|
|
28
|
+
destPath: string,
|
|
29
|
+
entryFile: string,
|
|
30
|
+
): Promise<void> {
|
|
31
|
+
let content = await fs.readFile(srcPath, "utf-8");
|
|
32
|
+
|
|
33
|
+
// Remove legacy script tags (e.g. referencing index.js, index_modern.js, etc.)
|
|
34
|
+
content = content.replace(
|
|
35
|
+
/<script\s+src=["'](?:index\.js|index_modern\.js)["']\s*><\/script>/gi,
|
|
36
|
+
"",
|
|
37
|
+
);
|
|
38
|
+
// Remove all HTML comments
|
|
39
|
+
content = content.replace(/<!--[\s\S]*?-->/g, "");
|
|
40
|
+
|
|
41
|
+
// Update inline script tags that dispatch 'odyssey:api' to be modules so they run after deferred module entry points.
|
|
42
|
+
content = content.replace(/<script([\s\S]*?)>([\s\S]*?)<\/script>/gi, (match, attributes, innerContent) => {
|
|
43
|
+
const hasOdysseyApi = innerContent.includes("odyssey:api");
|
|
44
|
+
const isClassicScript = !attributes.trim() || attributes.includes('type="text/javascript"') || attributes.includes("type='text/javascript'");
|
|
45
|
+
|
|
46
|
+
if (hasOdysseyApi && isClassicScript) {
|
|
47
|
+
return `<script type="module">${innerContent}</script>`;
|
|
48
|
+
}
|
|
49
|
+
return match;
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Inject the new Vite entry point
|
|
53
|
+
const scriptTag = `<script type="module" src="/${entryFile}"></script>`;
|
|
54
|
+
if (content.includes("</head>")) {
|
|
55
|
+
content = content.replace("</head>", ` ${scriptTag}\n </head>`);
|
|
56
|
+
} else {
|
|
57
|
+
content = scriptTag + "\n" + content;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
await fs.mkdir(path.dirname(destPath), { recursive: true });
|
|
61
|
+
await fs.writeFile(destPath, content);
|
|
62
|
+
await fs.unlink(srcPath);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The main entry point for the 'aunty migrate' command.
|
|
67
|
+
*/
|
|
68
|
+
export async function run(options: MigrateOptions = {}): Promise<number> {
|
|
69
|
+
intro(getHeader(pc.dim("aunty"), "migrate", { colour: "rainbow" }));
|
|
70
|
+
|
|
71
|
+
// 1. Load Project Details & Sanity Check on package.json presence
|
|
72
|
+
const details = await findProjectDetails(process.cwd());
|
|
73
|
+
if (!details) {
|
|
74
|
+
cancel(
|
|
75
|
+
"Could not find package.json. Please run this command inside an aunty project.",
|
|
76
|
+
);
|
|
77
|
+
return 1;
|
|
78
|
+
}
|
|
79
|
+
const baseDir = details.root;
|
|
80
|
+
|
|
81
|
+
// Check for aunty.config.js in the root of the repo
|
|
82
|
+
const configPath = path.join(baseDir, "aunty.config.js");
|
|
83
|
+
const hasConfig = await fs.access(configPath).then(() => true).catch(() => false);
|
|
84
|
+
if (hasConfig) {
|
|
85
|
+
log.error(
|
|
86
|
+
`Found legacy ${pc.cyan("aunty.config.js")} at the root of the project. We cannot migrate this project automatically. Please delete or rename the file before continuing, and configure Vite manually after migration.`
|
|
87
|
+
);
|
|
88
|
+
return 1;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// 2. Git Safety Checks (bypassed if options.skipGit is true)
|
|
92
|
+
if (!options.skipGit) {
|
|
93
|
+
const gitAccessible = await git.isAccessible();
|
|
94
|
+
const insideRepo = gitAccessible ? await git.isInsideRepo() : false;
|
|
95
|
+
|
|
96
|
+
if (!gitAccessible) {
|
|
97
|
+
log.error(
|
|
98
|
+
"Git is not accessible (please ensure git is installed and Xcode/licenses are accepted).",
|
|
99
|
+
);
|
|
100
|
+
return 1;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!insideRepo) {
|
|
104
|
+
log.error("The current directory is not a git repository.");
|
|
105
|
+
return 1;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check for uncommitted changes
|
|
109
|
+
if (!(await git.isClean())) {
|
|
110
|
+
log.error(
|
|
111
|
+
"You have uncommitted changes. Please commit or stash them before migrating.",
|
|
112
|
+
);
|
|
113
|
+
return 1;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check remote sync
|
|
117
|
+
if (await git.hasRemote()) {
|
|
118
|
+
if (await git.isBehindRemote()) {
|
|
119
|
+
log.error(
|
|
120
|
+
"Your local branch is behind the remote. Please pull the latest changes before migrating.",
|
|
121
|
+
);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// 3. Sanity Checks for index.html and index entrypoint
|
|
128
|
+
const publicHtmlPath = path.join(baseDir, "public/index.html");
|
|
129
|
+
try {
|
|
130
|
+
await fs.access(publicHtmlPath);
|
|
131
|
+
} catch {
|
|
132
|
+
log.error(
|
|
133
|
+
`Could not find public/index.html. This project does not seem to match the expected legacy structure.`,
|
|
134
|
+
);
|
|
135
|
+
return 1;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const hasTS = await fs
|
|
139
|
+
.access(path.join(baseDir, "src/index.ts"))
|
|
140
|
+
.then(() => true)
|
|
141
|
+
.catch(() => false);
|
|
142
|
+
const hasJS = await fs
|
|
143
|
+
.access(path.join(baseDir, "src/index.js"))
|
|
144
|
+
.then(() => true)
|
|
145
|
+
.catch(() => false);
|
|
146
|
+
|
|
147
|
+
if (!hasTS && !hasJS) {
|
|
148
|
+
log.error("Could not find src/index.ts or src/index.js entry point.");
|
|
149
|
+
return 1;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const entryFile = hasTS ? "src/index.ts" : "src/index.js";
|
|
153
|
+
|
|
154
|
+
log.message("Attempt to migrate an Aunty Webpack project to Aunty Vite.");
|
|
155
|
+
log.message(
|
|
156
|
+
"This is a fairly naive and destructive process, and may not work for your specific configuration.",
|
|
157
|
+
);
|
|
158
|
+
log.message(
|
|
159
|
+
"Please make take a backup or ensure Git is pushed before proceeding.",
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
// 4. Initial Confirmation Prompt (bypassed if options.yes is true)
|
|
163
|
+
if (!options.yes) {
|
|
164
|
+
const shouldContinue = await confirm({
|
|
165
|
+
message: "Are you sure you want to migrate this project to Vite?",
|
|
166
|
+
initialValue: true,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!shouldContinue || isCancel(shouldContinue)) {
|
|
170
|
+
cancel("Migration cancelled.");
|
|
171
|
+
return 0;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// 5. Migration Execution
|
|
176
|
+
const migrationSpinner = spin("Migrating configuration files...");
|
|
177
|
+
try {
|
|
178
|
+
const templateDir = path.resolve(
|
|
179
|
+
import.meta.dirname,
|
|
180
|
+
"../../../templates/svelte/base/contents",
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
// Copy Vite config files
|
|
184
|
+
const filesToCopy = [
|
|
185
|
+
"vite.config.ts",
|
|
186
|
+
"svelte.config.js",
|
|
187
|
+
"tsconfig.json",
|
|
188
|
+
"tsconfig.app.json",
|
|
189
|
+
"tsconfig.node.json",
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
for (const file of filesToCopy) {
|
|
193
|
+
const srcPath = path.join(templateDir, file);
|
|
194
|
+
const destPath = path.join(baseDir, file);
|
|
195
|
+
await fs.copyFile(srcPath, destPath);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// Merge .gitignore
|
|
199
|
+
const templateGitignorePath = path.join(templateDir, "_gitignore");
|
|
200
|
+
const targetGitignorePath = path.join(baseDir, ".gitignore");
|
|
201
|
+
let existingGitignore = "";
|
|
202
|
+
try {
|
|
203
|
+
existingGitignore = await fs.readFile(targetGitignorePath, "utf-8");
|
|
204
|
+
} catch {}
|
|
205
|
+
const templateGitignore = await fs.readFile(templateGitignorePath, "utf-8");
|
|
206
|
+
|
|
207
|
+
const uniqueRules = Array.from(
|
|
208
|
+
new Set(
|
|
209
|
+
[
|
|
210
|
+
...existingGitignore.split(/\r?\n/),
|
|
211
|
+
...templateGitignore.split(/\r?\n/),
|
|
212
|
+
]
|
|
213
|
+
.map((line) => line.trim())
|
|
214
|
+
.filter((line) => line && !line.startsWith("#")),
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
await fs.writeFile(targetGitignorePath, uniqueRules.join("\n") + "\n");
|
|
218
|
+
|
|
219
|
+
// Move index.html
|
|
220
|
+
const destHtmlPath = path.join(baseDir, "index.html");
|
|
221
|
+
await migrateHtmlFile(publicHtmlPath, destHtmlPath, entryFile);
|
|
222
|
+
|
|
223
|
+
// Check for legacy builder in public/builder/index.html
|
|
224
|
+
const publicBuilderHtmlPath = path.join(baseDir, "public/builder/index.html");
|
|
225
|
+
const hasLegacyBuilder = await fs
|
|
226
|
+
.access(publicBuilderHtmlPath)
|
|
227
|
+
.then(() => true)
|
|
228
|
+
.catch(() => false);
|
|
229
|
+
|
|
230
|
+
if (hasLegacyBuilder) {
|
|
231
|
+
const destBuilderHtmlPath = path.join(baseDir, "builder/index.html");
|
|
232
|
+
await migrateHtmlFile(publicBuilderHtmlPath, destBuilderHtmlPath, entryFile);
|
|
233
|
+
|
|
234
|
+
// Clean up legacy builder directory inside public if it exists and is empty
|
|
235
|
+
try {
|
|
236
|
+
await fs.rmdir(path.join(baseDir, "public/builder"));
|
|
237
|
+
} catch {}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
migrationSpinner.stop(
|
|
241
|
+
"Configuration files updated and index.html migrated successfully" +
|
|
242
|
+
(hasLegacyBuilder ? " (including builder)" : ""),
|
|
243
|
+
);
|
|
244
|
+
} catch (err: any) {
|
|
245
|
+
migrationSpinner.cancel("Failed during file migration step");
|
|
246
|
+
log.error(err.message || String(err));
|
|
247
|
+
return 1;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 6. Update package.json
|
|
251
|
+
const pkgSpinner = spin("Updating package.json dependencies and scripts...");
|
|
252
|
+
try {
|
|
253
|
+
const templateDir = path.resolve(
|
|
254
|
+
import.meta.dirname,
|
|
255
|
+
"../../../templates/svelte/base/contents",
|
|
256
|
+
);
|
|
257
|
+
const pkgPath = path.join(baseDir, "package.json");
|
|
258
|
+
const pkg = await loadJson<any>(pkgPath);
|
|
259
|
+
const templatePkg = await loadJson<any>(
|
|
260
|
+
path.join(templateDir, "package.json"),
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
pkg.type = "module";
|
|
264
|
+
|
|
265
|
+
// Update scripts
|
|
266
|
+
if (!pkg.scripts) pkg.scripts = {};
|
|
267
|
+
pkg.scripts.dev = "vite serve";
|
|
268
|
+
pkg.scripts.build = "vite build";
|
|
269
|
+
pkg.scripts.check =
|
|
270
|
+
"svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json";
|
|
271
|
+
pkg.scripts.format = "prettier --write .";
|
|
272
|
+
if (pkg.scripts.start === "aunty serve") {
|
|
273
|
+
delete pkg.scripts.start;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Merge dependencies & devDependencies
|
|
277
|
+
if (!pkg.dependencies) pkg.dependencies = {};
|
|
278
|
+
if (!pkg.devDependencies) pkg.devDependencies = {};
|
|
279
|
+
|
|
280
|
+
const oldDeps = { ...(pkg.dependencies || {}), ...(pkg.devDependencies || {}) };
|
|
281
|
+
const hasSassDep = "sass" in oldDeps || "node-sass" in oldDeps;
|
|
282
|
+
const scssFiles = await glob("src/**/*.s[ac]ss", { cwd: baseDir });
|
|
283
|
+
const hasSass = hasSassDep || scssFiles.length > 0;
|
|
284
|
+
|
|
285
|
+
Object.assign(pkg.dependencies, templatePkg.dependencies || {});
|
|
286
|
+
Object.assign(pkg.devDependencies, templatePkg.devDependencies || {});
|
|
287
|
+
|
|
288
|
+
if (hasSass) {
|
|
289
|
+
pkg.devDependencies["sass-embedded"] = "^1.83.0";
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Save package.json
|
|
293
|
+
await fs.writeFile(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
|
|
294
|
+
|
|
295
|
+
// Set @abcnews/aunty to latest or local file
|
|
296
|
+
await installAunty(baseDir);
|
|
297
|
+
|
|
298
|
+
pkgSpinner.stop("package.json updated");
|
|
299
|
+
} catch (err: any) {
|
|
300
|
+
pkgSpinner.cancel("Failed to update package.json");
|
|
301
|
+
log.error(err.message || String(err));
|
|
302
|
+
return 1;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// 7. NPM Install
|
|
306
|
+
const installSpinner = spin("Installing dependencies via npm...");
|
|
307
|
+
try {
|
|
308
|
+
await $({ cwd: baseDir })`npm install`.quiet();
|
|
309
|
+
installSpinner.stop("Dependencies installed");
|
|
310
|
+
} catch (err: any) {
|
|
311
|
+
installSpinner.cancel("Failed to install dependencies.");
|
|
312
|
+
log.error(`npm install failed: ${err.message || String(err)}`);
|
|
313
|
+
return 1;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
outro(
|
|
317
|
+
"Migration completed successfully! You can now run 'npm run dev' to start development.",
|
|
318
|
+
);
|
|
319
|
+
return 0;
|
|
320
|
+
}
|