Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatic installer #382

Merged
merged 20 commits into from
Nov 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
aba6629
feat(installer): #32 initial version for next.js
sdorra Oct 2, 2024
451f933
refactor(installer): #32 improve task api
sdorra Oct 8, 2024
75dbf47
test(installer): #32 add some more unit tests
sdorra Oct 8, 2024
52ca6d6
feat(installer): #32 add option for mdx sample content
sdorra Oct 8, 2024
6843cb9
fix(installer): #32 do not print packagemanager output
sdorra Oct 10, 2024
3b39c9d
fix(installer): #32 wait until packages are installed
sdorra Oct 10, 2024
751ce9c
feat(installer): #32 add options to migrators
sdorra Oct 10, 2024
cc07626
feat(installer): #32 add separate cli package for use with pnpx/npx
sdorra Oct 10, 2024
5d01d3d
test(installer): #32 add missing unit tests for reading package.json
sdorra Oct 24, 2024
36ae054
refactor(installer): #32 move parsing of options from cli to installer
sdorra Oct 26, 2024
b23521f
feat(installer): #32 add support for remix
sdorra Oct 26, 2024
d3f7015
feat(installer): #32 add support for qwik
sdorra Oct 29, 2024
7249a6b
feat(installer): #32 add suport for SolidStart
sdorra Oct 30, 2024
237ff8b
feat(installer): #32 add support for vite
sdorra Nov 5, 2024
ff040b7
feat(installer): #32 add support for sveltekit
sdorra Nov 6, 2024
145d1b2
test(installer): #32 add missing unit test for resolving options
sdorra Nov 13, 2024
32678fd
refactor(installer): #32 make install a sub command
sdorra Nov 13, 2024
4af5976
docs(installer): #32 add automatic installation to quick start
sdorra Nov 13, 2024
cf6fced
feat(website): enable smooth scrolling
sdorra Nov 13, 2024
e5270ad
build: update turborepo to v2.2.3
sdorra Nov 13, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .better-commits.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,10 @@
"value": "cli",
"label": "cli"
},
{
"value": "installer",
"label": "installer"
},
{
"value": "website",
"label": "website"
Expand Down
1 change: 1 addition & 0 deletions .commitlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
[
"core",
"cli",
"installer",
"website",
"vite",
"remix-vite",
Expand Down
33 changes: 32 additions & 1 deletion docs/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,38 @@ Content Collections transforms your content into type-safe data collections, pro

### Installation

Content Collection supports most of the major web frameworks. Choose the one you are using to get started.
Content Collection supports most major web frameworks. You can install it [automatically](#automatic-installation-experimental) (currently experimental) or [manually](#manual-installation).

#### Automatic Installation (experimental)

<Callout type="warn">
Automatic installation is experimental and may not work for every project.
Please ensure you have committed your latest changes before running the installation.

If you encounter any issues, create a [ticket](https://github.com/sdorra/content-collections/issues) and use the manual installation.
</Callout>

Content collections can be installed automatically using the `content-collections` package.
The automatic installation currently supports the following frameworks:

- Next.js
- Remix
- Qwik
- SvelteKit
- SolidStart
- Vite

To install Content Collections automatically, run the following command in your project's directory:

```bash
npx content-collections install
```

The setup will guide you through the installation process and execute the necessary steps to configure Content Collections for your project.

#### Manual Installation

Choose the one you are using to get started.

<QuickStartList />

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
"@content-collections/prettier-config": "workspace:*",
"@playwright/test": "^1.47.0",
"husky": "^9.0.11",
"turbo": "^2.0.14"
"turbo": "^2.2.3"
},
"prettier": "@content-collections/prettier-config",
"packageManager": "[email protected]"
Expand Down
37 changes: 37 additions & 0 deletions packages/content-collections/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
{
"name": "content-collections",
"description": "Installer for content-collections",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"content-collections": "./dist/index.js"
},
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"files": [
"dist",
"README.md"
],
"scripts": {
"build": "tsc"
},
"dependencies": {
"@content-collections/installer": "workspace:^",
"@inquirer/prompts": "^7.0.0",
"chalk": "^5.3.0",
"clerc": "^0.44.0",
"listr2": "^8.2.5",
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "20",
"typescript": "^5.6.3"
}
}
66 changes: 66 additions & 0 deletions packages/content-collections/src/commands/install.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#!/usr/bin/env node
import { createMigrator, type Option } from "@content-collections/installer";
import { input, select } from "@inquirer/prompts";
import chalk from "chalk";
import { Listr } from "listr2";

async function ask(option: Option) {
if (option.type === "list") {
const result = await select({
message: option.description || `Select an ${option.name}`,
default: option.defaultValue,
choices: option.choices,
});
if (typeof result !== "string") {
throw new Error("Invalid selection, the result must be a string");
}

return result;
}

return input({
default: option.defaultValue,
message: option.description || `Enter ${option.name}`,
});
}

export default async function install(directory: string) {
console.log("Searching for migrator in", directory);
const migrator = await createMigrator(directory);

console.log();
console.log(
"Found migrator",
chalk.bold(migrator.name) + ",",
"collecting options",
);
const migration = await migrator.createMigration(ask);
console.log();
console.log("Migration tasks:");

const tasks = new Listr(
migration.map((t) => ({
title: chalk.bold(t.name),
task: async (_, task) => {
const result = await t.run();
if (result.status === "skipped") {
task.skip(`${chalk.bold(t.name)}: ${result.message} [SKIPPED]`);
} else if (result.status === "error") {
throw new Error(`${chalk.bold(t.name)}: ${result.message} [ERROR]`);
}
},
})),
{
concurrent: false,
collectErrors: "minimal",
exitOnError: false,
},
);

await tasks.run();
if (tasks.errors.length > 0) {
console.log();
console.error("Errors occurred during migration");
process.exit(1);
}
}
34 changes: 34 additions & 0 deletions packages/content-collections/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env node

import { Clerc, completionsPlugin, helpPlugin, versionPlugin } from "clerc";
import install from "./commands/install.js";

import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
const packageJson = require("../package.json");

const name = "content-collections";
if (!packageJson.bin[name]) {
throw new Error(`Missing bin entry for ${name} in package.json`);
}

Clerc.create()
.scriptName(name)
.description(packageJson.description)
.version(packageJson.version)
.command("install", "Installs content-collection to a existing project", {
flags: {
directory: {
type: String,
description: "Path to the existing project",
default: ".",
},
},
})
.on("install", (context) => {
return install(context.flags.directory);
})
.use(helpPlugin())
.use(versionPlugin())
.use(completionsPlugin())
.parse();
26 changes: 26 additions & 0 deletions packages/content-collections/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"compilerOptions": {
/* Base Options: */
"esModuleInterop": true,
"skipLibCheck": true,
"target": "es2022",
"allowJs": true,
"resolveJsonModule": true,
"moduleDetection": "force",
"isolatedModules": true,
/* Strictness */
"strict": true,
"noUncheckedIndexedAccess": true,
/* ink */
"jsx": "react-jsx",
/* If transpiling with TypeScript: */
"moduleResolution": "NodeNext",
"module": "NodeNext",
"outDir": "dist",
"sourceMap": true,
/* If your code doesn't run in the DOM: */
"lib": ["es2022"],

"baseUrl": "."
}
}
39 changes: 39 additions & 0 deletions packages/installer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "@content-collections/installer",
"description": "CLI for installing content-collections",
"version": "0.1.0",
"type": "module",
"main": "dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
"./package.json": "./package.json",
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts -d dist",
"test": "vitest ./src --run",
"gen-content": "tsx ./scripts/gen-content.ts",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20.14.9",
"@vitest/coverage-v8": "^2.1.1",
"tsup": "^8.2.4",
"tsx": "^4.1.1",
"typescript": "^5.5.4",
"vitest": "^2.1.1",
"zod": "^3.23.8"
},
"dependencies": {
"@babel/parser": "^7.25.9",
"comment-json": "^4.2.5",
"package-manager-detector": "^0.2.0",
"recast": "^0.23.9"
}
}
32 changes: 32 additions & 0 deletions packages/installer/scripts/gen-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import fs from "fs/promises";
import { join } from "path";

function createObject(filename: string, content: string) {
// remove extension from filename
return `{
filename: "${filename.replace(".md", "")}",
content: ${JSON.stringify(content)},
},`;
}

async function generate(directory: string, varName: string) {
const filenames = (await fs.readdir(directory)).filter((filename) =>
filename.endsWith(".md"),
);

let content = `export const ${varName} = [\n`;
for (const filename of filenames) {
const filePath = join(directory, filename);
const fileContent = await fs.readFile(filePath, "utf-8");

const object = createObject(filename, fileContent);
content += object;
}

content += "];\n";

const tsFilePath = join(directory, "index.ts");
await fs.writeFile(tsFilePath, content);
}

generate("src/migration/content/posts", "allPosts");
23 changes: 23 additions & 0 deletions packages/installer/src/__tests__/tmpdir.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { test } from "vitest";

interface TmpDirFixture {
tmpdir: string;
}

// TODO: create a tests package to avoid duplication
export const tmpdirTest = test.extend<TmpDirFixture>({
tmpdir: async ({}, use) => {
const ostmpdir = os.tmpdir();

const directory = await fs.mkdtemp(path.join(ostmpdir, "vitest-"));
// we need to call realpath, because mktemp returns /var/folders/... on macOS
// but the paths which are returned by the watcher are /private/var/folders/...
const directoryPath = await fs.realpath(directory);
await use(directoryPath);

await fs.rm(directory, { recursive: true });
},
});
24 changes: 24 additions & 0 deletions packages/installer/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { findMigrator } from "./migration/index.js";
import { readPackageJson } from "./packageJson.js";
import { Ask, resolveOptions } from "./migration/options.js";
export type { Option, InputOption, ListOption } from "./migration/options.js";

export async function createMigrator(directory: string) {
const packageJson = await readPackageJson(directory);
const migrator = findMigrator(packageJson);

return {
name: migrator.name,
createMigration: async (ask: Ask) => {
const options = await resolveOptions(migrator.options, ask);

return migrator.createMigration(
{
directory,
packageJson,
},
options,
);
}
}
}
1 change: 1 addition & 0 deletions packages/installer/src/migration/content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { allPosts } from "./content/posts/index.js";
Loading
Loading