From f9c88f0a4225b736d67f5951f167ce34be51686a Mon Sep 17 00:00:00 2001 From: nshiab Date: Tue, 3 Mar 2026 10:48:58 -0500 Subject: [PATCH 1/2] Refactor: modularize codebase and add --agent flag --- .github/workflows/tests.yml | 35 + PLAN.md | 55 + README.md | 6 +- deno.json | 1 + deno.lock | 37 +- scripts/build.ts | 22 + setup-sda.mjs | 2382 ++++++++++------------------------- src/actions/agents.ts | 93 ++ src/actions/base.ts | 80 ++ src/actions/svelte.ts | 223 ++++ src/lib/utils.ts | 27 + src/main.ts | 47 + src/templates/configs.ts | 269 ++++ src/templates/css.ts | 853 +++++++++++++ src/templates/svelte.ts | 529 ++++++++ tests/agents_test.ts | 67 + tests/base_test.ts | 72 ++ tests/combinations_test.ts | 33 + tests/helpers.ts | 48 + tests/svelte_test.ts | 46 + 20 files changed, 3226 insertions(+), 1699 deletions(-) create mode 100644 .github/workflows/tests.yml create mode 100644 PLAN.md create mode 100644 scripts/build.ts create mode 100644 src/actions/agents.ts create mode 100644 src/actions/base.ts create mode 100644 src/actions/svelte.ts create mode 100644 src/lib/utils.ts create mode 100644 src/main.ts create mode 100644 src/templates/configs.ts create mode 100644 src/templates/css.ts create mode 100644 src/templates/svelte.ts create mode 100644 tests/agents_test.ts create mode 100644 tests/base_test.ts create mode 100644 tests/combinations_test.ts create mode 100644 tests/helpers.ts create mode 100644 tests/svelte_test.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..758d6cf --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,35 @@ +name: Tests + +on: + push: + branches: [main] + pull_request: + branches: [main] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Build + run: deno task build + + - name: Run Tests + run: deno test -A tests/ diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..e4c656e --- /dev/null +++ b/PLAN.md @@ -0,0 +1,55 @@ +# Refactoring Plan: setup-sda + +The goal is to transform the monolithic `setup-sda.mjs` into a modular, +testable, and maintainable TypeScript project while preserving its +"zero-install" capability for `npx`, `deno run`, and `bunx`. + +## Phase 1: Baseline Integration Testing (CRITICAL) +Establish a "green" state by verifying the current CLI behavior across all major +runtimes. + +- [x] **`tests/helpers.ts`**: Define `runCLI` and assertion logic. +- [x] **Modular Test Suites (Running on Deno, Node, Bun)**: + - [x] `tests/base_test.ts`: Base setup, `--git`, `--env`, `--scrape`. + - [x] `tests/agents_test.ts`: `--claude`, `--gemini`, `--copilot`. + - [x] `tests/svelte_test.ts`: `--svelte`, `--pages`, `--example` combinations. + - [x] `tests/combinations_test.ts`: "Kitchen sink" and edge-case flag + combinations. +- [x] **Refinement**: Implement a lightweight verification mode in tests that + skips heavy installations (if possible) while still checking file + generation. + +## Phase 2: Project Infrastructure & Build Pipeline +Set up the modern TypeScript structure and automated bundling. + +- [x] **Directories**: Create `src/main.ts`, `src/helpers/`, `src/templates/`, + `src/actions/`. +- [x] **Bundling Config**: + - [x] Add `build` task to `deno.json`: `deno bundle src/main.ts -o setup-sda.mjs`. + - [x] Create `scripts/build.ts` to wrap `deno bundle` and prepend the + `#!/usr/bin/env node` shebang. +- [x] **Module Discovery**: Ensure all runtime-specific modules are correctly + imported and bundled. + +## Phase 3: Template Extraction & New Features +Move large template strings into dedicated files and add the new feature. + +- [x] **CSS Templates**: `src/templates/css.ts` (style, highlight-theme). +- [x] **Svelte Templates**: `src/templates/svelte.ts` (components, pages, + layouts). +- [x] **Config Templates**: `src/templates/configs.ts` (vite, tsconfig, + deno/package.json). +- [x] **New Feature**: Add `--agent` flag support to generate `AGENTS.md`. +- [x] **Validation**: Run modular integration tests against a bundled version + containing extracted templates and the new `--agent` flag. + +## Phase 4: Logic Modularization & Final Polish +Break down the core logic into testable units. + +- [x] **Action Modules**: `src/actions/` (git, env, scrape, agents, svelte). +- [x] **Refinement**: Move core setup logic into standalone functions in + `src/actions/`. +- [x] **Cross-Runtime Check**: Final test run on Deno, Node, and Bun. +- [x] **Zero-Install Verification**: Simulate remote execution (`npx`, + `deno run -A ./...`). +- [x] **Cleanup**: Remove legacy monolith and temporary build artifacts. diff --git a/README.md b/README.md index ad4c46b..0f39e16 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ bunx --bun setup-sda Here are the different options: -- `--claude` or `--gemini` or `--copilot`: Adds a `CLAUDE.md` or `GEMINI.md` or - `.github/copilot-instructions.md` file and extra documentation in `./docs` to - work efficiently with AI agents. +- `--claude` or `--gemini` or `--copilot` or `--agent`: Adds a `CLAUDE.md` or + `GEMINI.md` or `.github/copilot-instructions.md` or `AGENTS.md` file and extra + documentation in `./docs` to work efficiently with AI agents. - `--example`: Adds example files. - `--scrape`: Adds web scraping dependencies. - `--svelte`: Adds a Svelte project. diff --git a/deno.json b/deno.json index 0cb86aa..86275db 100644 --- a/deno.json +++ b/deno.json @@ -3,6 +3,7 @@ "version": "1.14.15", "exports": "./setup-sda.mjs", "tasks": { + "build": "deno run -A scripts/build.ts", "patch": "deno run -A incrementVersion.js patch && npm publish", "minor": "deno run -A incrementVersion.js minor && npm publish", "major": "deno run -A incrementVersion.js major && npm publish" diff --git a/deno.lock b/deno.lock index 4fbc1a4..6ecadd8 100644 --- a/deno.lock +++ b/deno.lock @@ -1,5 +1,5 @@ { - "version": "4", + "version": "5", "specifiers": { "npm:@types/node@*": "22.12.0" }, @@ -13,5 +13,40 @@ "undici-types@6.20.0": { "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" } + }, + "remote": { + "https://deno.land/std@0.224.0/assert/_constants.ts": "a271e8ef5a573f1df8e822a6eb9d09df064ad66a4390f21b3e31f820a38e0975", + "https://deno.land/std@0.224.0/assert/assert.ts": "09d30564c09de846855b7b071e62b5974b001bb72a4b797958fe0660e7849834", + "https://deno.land/std@0.224.0/assert/assert_almost_equals.ts": "9e416114322012c9a21fa68e187637ce2d7df25bcbdbfd957cd639e65d3cf293", + "https://deno.land/std@0.224.0/assert/assert_array_includes.ts": "14c5094471bc8e4a7895fc6aa5a184300d8a1879606574cb1cd715ef36a4a3c7", + "https://deno.land/std@0.224.0/assert/assert_equals.ts": "3bbca947d85b9d374a108687b1a8ba3785a7850436b5a8930d81f34a32cb8c74", + "https://deno.land/std@0.224.0/assert/assert_exists.ts": "43420cf7f956748ae6ed1230646567b3593cb7a36c5a5327269279c870c5ddfd", + "https://deno.land/std@0.224.0/assert/assert_false.ts": "3e9be8e33275db00d952e9acb0cd29481a44fa0a4af6d37239ff58d79e8edeff", + "https://deno.land/std@0.224.0/assert/assert_greater.ts": "5e57b201fd51b64ced36c828e3dfd773412c1a6120c1a5a99066c9b261974e46", + "https://deno.land/std@0.224.0/assert/assert_greater_or_equal.ts": "9870030f997a08361b6f63400273c2fb1856f5db86c0c3852aab2a002e425c5b", + "https://deno.land/std@0.224.0/assert/assert_instance_of.ts": "e22343c1fdcacfaea8f37784ad782683ec1cf599ae9b1b618954e9c22f376f2c", + "https://deno.land/std@0.224.0/assert/assert_is_error.ts": "f856b3bc978a7aa6a601f3fec6603491ab6255118afa6baa84b04426dd3cc491", + "https://deno.land/std@0.224.0/assert/assert_less.ts": "60b61e13a1982865a72726a5fa86c24fad7eb27c3c08b13883fb68882b307f68", + "https://deno.land/std@0.224.0/assert/assert_less_or_equal.ts": "d2c84e17faba4afe085e6c9123a63395accf4f9e00150db899c46e67420e0ec3", + "https://deno.land/std@0.224.0/assert/assert_match.ts": "ace1710dd3b2811c391946954234b5da910c5665aed817943d086d4d4871a8b7", + "https://deno.land/std@0.224.0/assert/assert_not_equals.ts": "78d45dd46133d76ce624b2c6c09392f6110f0df9b73f911d20208a68dee2ef29", + "https://deno.land/std@0.224.0/assert/assert_not_instance_of.ts": "3434a669b4d20cdcc5359779301a0588f941ffdc2ad68803c31eabdb4890cf7a", + "https://deno.land/std@0.224.0/assert/assert_not_match.ts": "df30417240aa2d35b1ea44df7e541991348a063d9ee823430e0b58079a72242a", + "https://deno.land/std@0.224.0/assert/assert_not_strict_equals.ts": "37f73880bd672709373d6dc2c5f148691119bed161f3020fff3548a0496f71b8", + "https://deno.land/std@0.224.0/assert/assert_object_match.ts": "411450fd194fdaabc0089ae68f916b545a49d7b7e6d0026e84a54c9e7eed2693", + "https://deno.land/std@0.224.0/assert/assert_rejects.ts": "4bee1d6d565a5b623146a14668da8f9eb1f026a4f338bbf92b37e43e0aa53c31", + "https://deno.land/std@0.224.0/assert/assert_strict_equals.ts": "b4f45f0fd2e54d9029171876bd0b42dd9ed0efd8f853ab92a3f50127acfa54f5", + "https://deno.land/std@0.224.0/assert/assert_string_includes.ts": "496b9ecad84deab72c8718735373feb6cdaa071eb91a98206f6f3cb4285e71b8", + "https://deno.land/std@0.224.0/assert/assert_throws.ts": "c6508b2879d465898dab2798009299867e67c570d7d34c90a2d235e4553906eb", + "https://deno.land/std@0.224.0/assert/assertion_error.ts": "ba8752bd27ebc51f723702fac2f54d3e94447598f54264a6653d6413738a8917", + "https://deno.land/std@0.224.0/assert/equal.ts": "bddf07bb5fc718e10bb72d5dc2c36c1ce5a8bdd3b647069b6319e07af181ac47", + "https://deno.land/std@0.224.0/assert/fail.ts": "0eba674ffb47dff083f02ced76d5130460bff1a9a68c6514ebe0cdea4abadb68", + "https://deno.land/std@0.224.0/assert/mod.ts": "48b8cb8a619ea0b7958ad7ee9376500fe902284bb36f0e32c598c3dc34cbd6f3", + "https://deno.land/std@0.224.0/assert/unimplemented.ts": "8c55a5793e9147b4f1ef68cd66496b7d5ba7a9e7ca30c6da070c1a58da723d73", + "https://deno.land/std@0.224.0/assert/unreachable.ts": "5ae3dbf63ef988615b93eb08d395dda771c96546565f9e521ed86f6510c29e19", + "https://deno.land/std@0.224.0/fmt/colors.ts": "508563c0659dd7198ba4bbf87e97f654af3c34eb56ba790260f252ad8012e1c5", + "https://deno.land/std@0.224.0/internal/diff.ts": "6234a4b493ebe65dc67a18a0eb97ef683626a1166a1906232ce186ae9f65f4e6", + "https://deno.land/std@0.224.0/internal/format.ts": "0a98ee226fd3d43450245b1844b47003419d34d210fa989900861c79820d21c2", + "https://deno.land/std@0.224.0/internal/mod.ts": "534125398c8e7426183e12dc255bb635d94e06d0f93c60a297723abe69d3b22e" } } diff --git a/scripts/build.ts b/scripts/build.ts new file mode 100644 index 0000000..77a8e49 --- /dev/null +++ b/scripts/build.ts @@ -0,0 +1,22 @@ +const bundleCommand = new Deno.Command("deno", { + args: ["bundle", "src/main.ts", "-o", "setup-sda.mjs"], +}); + +const { success, stderr } = await bundleCommand.output(); + +if (!success) { + console.error(new TextDecoder().decode(stderr)); + Deno.exit(1); +} + +const content = await Deno.readTextFile("setup-sda.mjs"); +await Deno.writeTextFile("setup-sda.mjs", `#!/usr/bin/env node + +${content}`); + +const chmodCommand = new Deno.Command("chmod", { + args: ["+x", "setup-sda.mjs"], +}); +await chmodCommand.output(); + +console.log("Build complete!"); diff --git a/setup-sda.mjs b/setup-sda.mjs index 0ca2aa7..6bccc22 100755 --- a/setup-sda.mjs +++ b/setup-sda.mjs @@ -1,57 +1,39 @@ #!/usr/bin/env node -import { execSync } from "node:child_process"; -import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; -import { Buffer } from "node:buffer"; +// src/main.ts import process from "node:process"; -console.log("\nStarting sda setup..."); - -console.log("\n1 - Checking runtime and options..."); - -let runtime; -console.log(` => Your navigator userAgent is: ${navigator.userAgent}`); -const userAgent = navigator.userAgent.toLocaleLowerCase(); -if (userAgent.includes("bun")) { - runtime = "bun"; -} else if (userAgent.includes("deno")) { - runtime = "deno"; -} else if (userAgent.includes("node")) { - runtime = "nodejs"; -} else { - throw new Error("Unknown runtime."); -} - -const args = process.argv.slice(2); - -if (args.includes("--git")) { - console.log(` => You passed the option --git`); -} -if (args.includes("--scrape")) { - console.log(` => You passed the option --scrape`); -} -if (args.includes("--pages") && args.includes("--svelte")) { - console.log(` => You passed the option --pages`); -} else if (args.includes("--pages") && !args.includes("--svelte")) { - console.log( - ` => You passed the option --pages, but it only works with --svelte`, - ); -} -if (args.includes("--env")) { - console.log(` => You passed the option --env`); -} -if (args.includes("--claude")) { - console.log(` => You passed the option --claude`); -} -if (args.includes("--gemini")) { - console.log(` => You passed the option --gemini`); -} -if (args.includes("--copilot")) { - console.log(` => You passed the option --copilot`); +// src/lib/utils.ts +import { execSync as nodeExecSync } from "node:child_process"; +function getRuntime() { + const userAgent = (globalThis.navigator?.userAgent || "node").toLocaleLowerCase(); + if (userAgent.includes("bun")) return "bun"; + if (userAgent.includes("deno")) return "deno"; + return "nodejs"; +} +function createExecSync(isTest) { + return (command, options = { + stdio: "ignore" + }) => { + if (isTest && (command.includes("npm ") || command.includes("npx ") || command.includes("bun ") || command.includes("bunx ") || command.includes("deno install"))) { + console.log(` => Skipping: ${command}`); + } else { + nodeExecSync(command, options); + } + }; } -let llm = - `Always verify if there is a deno.json or package.json file in the root of the project and familiarize yourself with the scripts available in it and the libraries already installed in the project. +// src/actions/agents.ts +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +async function setupAgents(args) { + if (!args.includes("--claude") && !args.includes("--gemini") && !args.includes("--copilot") && !args.includes("--agent")) return; + console.log(" => Fetching up-to-date documentation for journalism..."); + const journalismDoc = await fetch("https://raw.githubusercontent.com/nshiab/journalism/refs/heads/main/llm.md").then((d) => d.text()); + const journalismFunctions = journalismDoc.split("\n").filter((line) => line.startsWith("## ")).map((line) => line.replace("## ", "").trim()).join("\n"); + console.log(" => Fetching up-to-date documentation for simple-data-analysis..."); + const sdaDoc = await fetch("https://raw.githubusercontent.com/nshiab/simple-data-analysis/refs/heads/main/llm.md").then((d) => d.text()); + const sdaClassesAndMethods = sdaDoc.split("\n").filter((line) => line.startsWith("## ") || line.startsWith("#### ")).map((line) => line.startsWith("## ") ? "\n" + line.replace("## ", "").trim() : line.replace("#### Parameters", " Methods:").replace("#### ", " ").replaceAll("`", "")).join("\n"); + const llm = `Always verify if there is a deno.json or package.json file in the root of the project and familiarize yourself with the scripts available in it and the libraries already installed in the project. If it's a Deno project, always run \`deno run -A --node-modules-dir=auto --env-file --check sda/main.ts\` to test your code. Before handing off your work, always run \`deno lint\` and \`deno fmt\` as well. Fix any errors or warnings triggered along the way. @@ -73,452 +55,58 @@ import { SimpleDB } from "@nshiab/simple-data-analysis"; Here are the functions available in the "journalism" library. If one of the function might be relevant, read the complete documentation at "./docs/journalism.md" to properly use it. -{journalismFunctions} +\${journalismFunctions} Here are the classes and their methods available in the "simple-data-analysis" library. If one of the classes or methods might be relevant, read the complete documentation at "./docs/simple-data-analysis.md" to properly use it. Remember that almost all methods are asynchronous, so you need to use \`await\` when calling them. -{sdaClassesAndMethods}`; - -if ( - args.includes("--claude") || args.includes("--gemini") || - args.includes("--copilot") -) { - console.log(" => Fetching up-to-date documentation for journalism..."); - const journalismDoc = await fetch( - "https://raw.githubusercontent.com/nshiab/journalism/refs/heads/main/llm.md", - ).then((d) => d.text()); - - const journalismFunctions = journalismDoc - .split("\n") - .filter((line) => line.startsWith("## ")) - .map((line) => line.replace("## ", "").trim()) - .join("\n"); - - llm = llm.replace("{journalismFunctions}", journalismFunctions); - - console.log( - " => Fetching up-to-date documentation for simple-data-analysis...", - ); - const sdaDoc = await fetch( - "https://raw.githubusercontent.com/nshiab/simple-data-analysis/refs/heads/main/llm.md", - ).then((d) => d.text()); - - const sdaClassesAndMethods = sdaDoc - .split("\n") - .filter((line) => line.startsWith("## ") || line.startsWith("#### ")) - .map((line) => - line.startsWith("## ") - ? "\n" + line.replace("## ", "").trim() - : line.replace("#### Parameters", " Methods:").replace("#### ", " ") - .replaceAll("`", "") - ) - .join("\n"); - - llm = llm.replace("{sdaClassesAndMethods}", sdaClassesAndMethods); - - if (args.includes("--claude")) { - if (existsSync("CLAUDE.md")) { - console.log(" => CLAUDE.md already exists. Skipping creation."); - } else { - console.log(" => Creating CLAUDE.md."); - writeFileSync("CLAUDE.md", llm); +\${sdaClassesAndMethods}`; + const agentFiles = [ + { + flag: "--claude", + file: "CLAUDE.md" + }, + { + flag: "--gemini", + file: "GEMINI.md" + }, + { + flag: "--agent", + file: "AGENTS.md" } - } - if (args.includes("--gemini")) { - if (existsSync("GEMINI.md")) { - console.log(" => GEMINI.md already exists. Skipping creation."); - } else { - console.log(" => Creating GEMINI.md."); - writeFileSync("GEMINI.md", llm); + ]; + for (const { flag, file } of agentFiles) { + if (args.includes(flag)) { + if (existsSync(file)) { + console.log(" => " + file + " already exists. Skipping creation."); + } else { + console.log(" => Creating " + file + "."); + writeFileSync(file, llm); + } } } if (args.includes("--copilot")) { - if (existsSync(".github")) { - console.log(" => .github folder already exists. Skipping creation."); - if (existsSync(".github/copilot-instructions.md")) { - console.log( - " => .github/copilot-instructions.md already exists. Skipping creation.", - ); - } else { - console.log(" => Creating .github/copilot-instructions.md."); - writeFileSync(".github/copilot-instructions.md", llm); - } + if (!existsSync(".github")) mkdirSync(".github", { + recursive: true + }); + if (existsSync(".github/copilot-instructions.md")) { + console.log(" => .github/copilot-instructions.md already exists. Skipping creation."); } else { - console.log(" => Creating .github folder."); - mkdirSync(".github"); - console.log( - " => Writing Copilot instructions to .github/copilot-instructions.md.", - ); + console.log(" => Creating .github/copilot-instructions.md."); writeFileSync(".github/copilot-instructions.md", llm); } } - - if (!existsSync("docs")) { - console.log(" => Creating docs folder."); - mkdirSync("docs"); - console.log(" => Writing documentation to docs/journalism.md."); - writeFileSync( - "docs/journalism.md", - journalismDoc, - ); - console.log( - " => Writing documentation to docs/simple-data-analysis.md.", - ); - writeFileSync( - "docs/simple-data-analysis.md", - sdaDoc, - ); - } -} - -if (args.includes("--svelte")) { - console.log(` => You passed the option --svelte`); - - const example = args.includes("--example"); - - const readme = - `This repository has been created with [setup-sda](https://github.com/nshiab/setup-sda/). - -It's using [simple-data-analysis](https://github.com/nshiab/simple-data-analysis), [journalism](https://github.com/nshiab/journalism), and others great open-source librairies with [SvelteKit](https://svelte.dev/docs/kit/introduction). - -Here's the recommended workflow: - -- Put your raw data in the \`sda/data\` folder. Note that this folder is gitignored. -- Use the \`sda/main.ts\` file to clean and process your raw data. Write the results to the \`src/data\` or \`static/\` folders. -- Import your processed data from the \`src/data\` folder into the \`src/routes/+page.svelte\` or fetch it with \`src/routes/+page.ts\`. -- Use the data for your content. - -When working on your project, you can use the following commands: - -- \`${ - runtime === "deno" - ? "deno task" - : "npm run" - } sda\` will watch your \`sda/main.ts\` and its dependencies. Everytime you'll save some changes, the data will be reprocessed. -- \`${ - runtime === "deno" - ? "deno task" - : "npm run" - } dev\` will start a local server and watch all \`src/*\` files and their dependencies. Everytime you'll save some changes or the data is reprocessed, the content will be updated. - -By opening two terminals each running one of the above commands, you'll be able to work on your project with a live preview of your content and data. - `; - - const packageJson = { - type: "module", - scripts: { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": - "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - sda: `node ${ - args.includes("--env") ? "--env-file=.env " : "" - }--experimental-strip-types --no-warnings --watch sda/main.ts`, - clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp", - }, - }; - - const denoJson = { - tasks: { - "dev": "vite dev", - "build": "vite build", - "preview": "vite preview", - "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", - "check:watch": - "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", - sda: `deno run ${ - args.includes("--env") ? "--env-file " : "" - }--node-modules-dir=auto -A --watch --check sda/main.ts`, - clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp", - }, - nodeModulesDir: "auto", - "unstable": ["sloppy-imports", "fmt-component"], - "lint": { - "rules": { - "exclude": ["no-sloppy-imports"], - }, - }, - "imports": { - "$lib": "./src/lib/index.ts", - "$lib/": "./src/lib/", - }, - "compilerOptions": { - "rootDirs": [ - ".", - "./.svelte-kit/types", - ], - "verbatimModuleSyntax": true, - "lib": [ - "esnext", - "DOM", - "DOM.Iterable", - "deno.ns", - ], - "types": [ - "./.svelte-kit/ambient.d.ts", - "./.svelte-kit/non-ambient.d.ts", - ], - }, - }; - - const ambientTypes = ``; - - const nonAmbientTypes = ``; - - const tempCsv = `city,decade,meanTemp -Toronto,1840.0,7.1 -Toronto,1850.0,6.4 -Toronto,1860.0,6.9 -Toronto,1870.0,6.8 -Montreal,1880.0,5.3 -Toronto,1880.0,6.6 -Montreal,1890.0,6.0 -Toronto,1890.0,7.6 -Montreal,1900.0,5.8 -Toronto,1900.0,7.7 -Montreal,1910.0,6.1 -Toronto,1910.0,8.1 -Montreal,1920.0,6.2 -Toronto,1920.0,8.0 -Montreal,1930.0,6.8 -Toronto,1930.0,8.6 -Montreal,1940.0,6.9 -Toronto,1940.0,8.7 -Montreal,1950.0,7.4 -Toronto,1950.0,9.2 -Montreal,1960.0,7.4 -Toronto,1960.0,8.8 -Montreal,1970.0,7.3 -Toronto,1970.0,9.0 -Montreal,1980.0,7.7 -Toronto,1980.0,9.0 -Toronto,1990.0,9.6 -Montreal,2000.0,7.6 -Toronto,2000.0,9.7 -Montreal,2010.0,8.0 -Toronto,2010.0,9.9`; - - const tempJSON = `[ - {"city":"Toronto","decade":1840,"meanTemp":7.1}, - {"city":"Toronto","decade":1850,"meanTemp":6.4}, - {"city":"Toronto","decade":1860,"meanTemp":6.9}, - {"city":"Toronto","decade":1870,"meanTemp":6.8}, - {"city":"Montreal","decade":1880,"meanTemp":5.3}, - {"city":"Toronto","decade":1880,"meanTemp":6.6}, - {"city":"Montreal","decade":1890,"meanTemp":6.0}, - {"city":"Toronto","decade":1890,"meanTemp":7.6}, - {"city":"Montreal","decade":1900,"meanTemp":5.8}, - {"city":"Toronto","decade":1900,"meanTemp":7.7}, - {"city":"Montreal","decade":1910,"meanTemp":6.1}, - {"city":"Toronto","decade":1910,"meanTemp":8.1}, - {"city":"Montreal","decade":1920,"meanTemp":6.2}, - {"city":"Toronto","decade":1920,"meanTemp":8.0}, - {"city":"Montreal","decade":1930,"meanTemp":6.8}, - {"city":"Toronto","decade":1930,"meanTemp":8.6}, - {"city":"Montreal","decade":1940,"meanTemp":6.9}, - {"city":"Toronto","decade":1940,"meanTemp":8.7}, - {"city":"Montreal","decade":1950,"meanTemp":7.4}, - {"city":"Toronto","decade":1950,"meanTemp":9.2}, - {"city":"Montreal","decade":1960,"meanTemp":7.4}, - {"city":"Toronto","decade":1960,"meanTemp":8.8}, - {"city":"Montreal","decade":1970,"meanTemp":7.3}, - {"city":"Toronto","decade":1970,"meanTemp":9.0}, - {"city":"Montreal","decade":1980,"meanTemp":7.7}, - {"city":"Toronto","decade":1980,"meanTemp":9.0}, - {"city":"Toronto","decade":1990,"meanTemp":9.6}, - {"city":"Montreal","decade":2000,"meanTemp":7.6}, - {"city":"Toronto","decade":2000,"meanTemp":9.7}, - {"city":"Montreal","decade":2010,"meanTemp":8.0}, - {"city":"Toronto","decade":2010,"meanTemp":9.9} -]`; - - const tempRegressionsJSON = `[ - {"city":"Toronto","x":"decade","y":"meanTemp","slope":0.0203,"yIntercept":-30.7911,"r2":0.9261}, - {"city":"Montreal","x":"decade","y":"meanTemp","slope":0.0196,"yIntercept":-31.3115,"r2":0.921} -]`; - - const mainTs = example - ? `import { SimpleDB } from "@nshiab/simple-data-analysis"; -import crunchData from "./helpers/crunchData.ts"; - -const sdb = new SimpleDB({ logDuration: true }); - -await crunchData(sdb); - -await sdb.done();` - : `import { SimpleDB } from "@nshiab/simple-data-analysis"; - -const sdb = new SimpleDB(); - -// Do your magic here... - -await sdb.done();`; - - const crunchDataTs = - `import type { SimpleDB } from "@nshiab/simple-data-analysis"; - -export default async function crunchData(sdb: SimpleDB) { - - // The mean temperature per decade. - const temp = sdb.newTable("temp"); - await temp.loadData("sda/data/temp.csv"); - await temp.logTable(); - - // We compute a linear regression for each city. - const tempRegressions = await temp.linearRegressions({ - x: "decade", - y: "meanTemp", - categories: "city", - decimals: 4, - outputTable: "tempRegressions", + if (!existsSync("docs")) mkdirSync("docs", { + recursive: true }); - await tempRegressions.logTable(); - - // We write the results to src/data - await tempRegressions.writeData("src/data/temp-regressions.json"); - // Or to static - await temp.writeData("static/temp.json"); -}`; - - const viteConfigTs = `import { sveltekit } from "@sveltejs/kit/vite"; -import { defineConfig } from "vite"; - -export default defineConfig({ - plugins: [sveltekit()], -}); - `; - - const tsconfigJson = `{ - "extends": "./.svelte-kit/tsconfig.json", - "compilerOptions": { - "allowJs": true, - "checkJs": true, - "esModuleInterop": true, - "forceConsistentCasingInFileNames": true, - "resolveJsonModule": true, - "skipLibCheck": true, - "sourceMap": true, - "strict": true, - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - } - // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias - // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files - // - // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes - // from the referenced tsconfig.json - TypeScript does not merge them in + writeFileSync("docs/journalism.md", journalismDoc); + writeFileSync("docs/simple-data-analysis.md", sdaDoc); } -`; - - const svelteConfigJs = args.includes("--pages") - ? `import adapter from "@sveltejs/adapter-static"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; -import process from "node:process"; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter(), - paths: { - base: process.argv.includes("dev") ? "" : process.env.BASE_PATH, - }, - }, -}; - -export default config;` - : `import adapter from "@sveltejs/adapter-static"; -import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - -/** @type {import('@sveltejs/kit').Config} */ -const config = { - preprocess: vitePreprocess(), - kit: { - adapter: adapter(), - }, -}; - -export default config;`; - - const deployYml = `name: Deploy to GitHub Pages - -on: - push: - branches: "main" - -jobs: - build_site: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Install Deno - uses: denoland/setup-deno@v2 - with: - deno-version: v2.x - - - name: Install dependencies - run: deno install - - - name: build - env: - BASE_PATH: "/\${{ github.event.repository.name }}" - run: deno task build - - - name: Upload Artifacts - uses: actions/upload-pages-artifact@v3 - with: - path: "build/" - - deploy: - needs: build_site - runs-on: ubuntu-latest - - permissions: - pages: write - id-token: write - - environment: - name: github-pages - url: \${{ steps.deployment.outputs.page_url }} - - steps: - - name: Deploy - id: deployment - uses: actions/deploy-pages@v4 -`; - - const gitignore = `node_modules - -# Output -.output -.vercel -/.svelte-kit -/build - -# OS -.DS_Store -Thumbs.db - -# Env -.env -.env.* -!.env.example -!.env.test - -# Vite -vite.config.js.timestamp-* -vite.config.ts.timestamp-* -# Added by setup-sda -.tmp -.sda-cache -.journalism-cache -sda/data`; +// src/actions/svelte.ts +import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync as writeFileSync2 } from "node:fs"; +import { Buffer } from "node:buffer"; - const styleCss = `/* Adapted from https://github.com/kevquirk/simple.css */ +// src/templates/css.ts +var styleCss = `/* Adapted from https://github.com/kevquirk/simple.css */ /* Global variables. */ :root { @@ -1244,8 +832,7 @@ sub { margin: 2rem 0; } `; - - const highlightThemeCss = `/* +var highlightThemeCss = `/* Theme: GitHub Description: Light theme as seen on github.com Author: github.com @@ -1372,38 +959,10 @@ sub { } */ `; - const appHtml = ` - - - - - - - %sveltekit.head% - - -
%sveltekit.body%
- - -`; - - const appDTs = `// See https://svelte.dev/docs/kit/types#app.d.ts -// for information about these interfaces -declare global { - namespace App { - // interface Error {} - // interface Locals {} - // interface PageData {} - // interface PageState {} - // interface Platform {} - } -} - -export {}; -`; - - const pageSvelte = example - ? ` +<\/script>

Climate change

@@ -1474,7 +1033,7 @@ export default async function crunchData(sdb: SimpleDB) { await temp.logLineChart("decade", "meanTemp", { smallMultiples: "city", fixedScales: true, - formatY: (d) => \\\`\\\${d}°C\\\`, + formatY: (d) => \\\`\\\${d}\xB0C\\\`, }); // We compute a linear regression for each city. @@ -1515,51 +1074,27 @@ export default async function crunchData(sdb: SimpleDB) { label="Pick an option:" name="radioButtonsOptions" /> -` - : `

Your new project

+`; + } else { + return `

Your new project

Time to create something amazing.

`; + } +} +function getLayoutSvelte(example, pages) { + if (example) { + return ` - - - ${ - args.includes("--pages") - ? '\n' - : '\n' - } - - + +

My new project

@@ -1577,10 +1112,11 @@ export const load: PageLoad = async ({ fetch }) => { >.

-` - : ` +<\/script> @@ -1590,48 +1126,10 @@ export const load: PageLoad = async ({ fetch }) => {
{@render children()}
- `; - - const libIndexTs = example - ? `// place files you want to import through the \`$lib\` alias in this folder. - -type cityT = string; - -type tempT = { city: string; decade: number; meanTemp: number }[]; - -type tempRegrT = { - city: string; - slope: number; - yIntercept: number; - r2: number; -}[]; - -export type { cityT, tempRegrT, tempT };` - : `// place files you want to import through the \`$lib\` alias in this folder.`; - - const getTempChangeTs = - `// It's important to use the 'web' entrypoint since this is running in the browser. -import { formatNumber } from "@nshiab/journalism/web"; -import type { tempRegrT } from "../lib/index.ts"; - -export default function getTempChange( - city: string, - tempRegr: tempRegrT, -): string { - const cityRegression = tempRegr.find((r) => r.city === city); - - if (cityRegression === undefined) { - throw new Error(\`City \${city} not found in tempRegr.\`); } - - const slopPerDecade = cityRegression.slope * 10; - - return formatNumber(slopPerDecade, { decimals: 3, suffix: "°C" }); } -`; - - const tableSvelte = ` +<\/script> {#if data.length > 5}
@@ -1699,8 +1197,7 @@ export default function getTempChange( Displaying {Math.max(0, endRow + 1 - startRow)} items out of {data.length}.

{/if}`; - - const selectSvelte = ` +<\/script> `; - - const radioSvelte = ` +<\/script>

{label}

{#each values as val, i} -
`; - - const CodeHighlightSvelte = ` +<\/script>

`; - - const highlightSvelte = ` +<\/script> {text} `; - - const chartSvelte = ` +<\/script> {#key [city, width]}
{/key}`; +var pageTs = `// Types automatically generated by SvelteKit. +import type { PageLoad } from "./$types"; +// Custom types. +import type { tempT } from "$lib"; - console.log("\n2 - Creating relevant files..."); - if (existsSync("README.md")) { - console.log(" => README.md already exists."); - } else { - writeFileSync("README.md", readme); - console.log(" => README.md has been created."); - } +export const load: PageLoad = async ({ fetch }) => { + const res = await fetch("/temp.json"); + const temp = await res.json() as tempT; - if (runtime === "bun") { - packageJson.scripts.sda = "bun --watch sda/main.ts"; - } + return { temp }; +}; +`; +var layoutTs = `export const prerender = true; +export const trailingSlash = "always";`; +function getLibIndexTs(example) { + if (example) { + return `// place files you want to import through the $lib alias in this folder. - if (runtime === "deno") { - if (existsSync("deno.json")) { - console.log(" => deno.json already exists."); - const currentDenoJson = JSON.parse( - readFileSync("deno.json", "utf-8"), - ); - denoJson.imports = { - ...currentDenoJson.imports, - ...denoJson.imports, - }; - currentDenoJson.tasks = { - ...currentDenoJson.tasks, - ...denoJson.tasks, - }; - writeFileSync("deno.json", JSON.stringify(currentDenoJson, null, 2)); - console.log(" => deno.json has been modified."); - } else { - writeFileSync("deno.json", JSON.stringify(denoJson, null, 2)); - console.log(" => deno.json has been created."); - } - } else { - if (existsSync("package.json")) { - console.log(" => package.json already exists."); - const currentPackageJson = JSON.parse( - readFileSync("package.json", "utf-8"), - ); - packageJson.dependencies = { - ...currentPackageJson.dependencies, - ...packageJson.dependencies, - }; - if (currentPackageJson.devDependencies) { - packageJson.devDependencies = { - ...currentPackageJson.devDependencies, - }; - } - currentPackageJson.scripts = { - ...currentPackageJson.scripts, - ...packageJson.scripts, - }; - writeFileSync( - "package.json", - JSON.stringify(packageJson, null, 2), - ); - console.log( - " => package.json has been modified.", - ); - } else { - writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); - console.log(" => package.json has been created."); - } - } +type cityT = string; - if (existsSync("vite.config.ts")) { - console.log(" => vite.config.ts already exists."); - } else { - writeFileSync("vite.config.ts", viteConfigTs); - console.log(" => vite.config.ts has been created."); - } +type tempT = { city: string; decade: number; meanTemp: number }[]; - if (existsSync("tsconfig.json")) { - console.log(" => tsconfig.json already exists."); - } else { - writeFileSync("tsconfig.json", tsconfigJson); - console.log(" => tsconfig.json has been created."); - } +type tempRegrT = { + city: string; + slope: number; + yIntercept: number; + r2: number; +}[]; - if (existsSync("svelte.config.js")) { - console.log(" => svelte.config.js already exists."); +export type { cityT, tempRegrT, tempT };`; } else { - writeFileSync("svelte.config.js", svelteConfigJs); - console.log(" => svelte.config.js has been created."); + return `// place files you want to import through the $lib alias in this folder.`; } +} +var getTempChangeTs = `// It's important to use the 'web' entrypoint since this is running in the browser. +import { formatNumber } from "@nshiab/journalism/web"; +import type { tempRegrT } from "../lib/index.ts"; - if (existsSync(".svelte-kit")) { - console.log(" => .svelte-kit/ already exists."); - } else { - mkdirSync(".svelte-kit"); - writeFileSync(".svelte-kit/ambient.d.ts", ambientTypes); - console.log(" => .svelte-kit/ambient.d.ts has been created."); - writeFileSync(".svelte-kit/non-ambient.d.ts", nonAmbientTypes); - console.log(" => .svelte-kit/non-ambient.d.ts has been created."); - } +export default function getTempChange( + city: string, + tempRegr: tempRegrT, +): string { + const cityRegression = tempRegr.find((r) => r.city === city); - if (existsSync(".gitignore")) { - console.log(" => .gitignore already exists. Appending to it."); - const currentGitignore = readFileSync(".gitignore", "utf-8"); - writeFileSync( - ".gitignore", - currentGitignore + "\n\n# Added by setup-sda\n" + - gitignore, - ); - } else { - writeFileSync(".gitignore", gitignore); - console.log(" => .gitignore has been created."); + if (cityRegression === undefined) { + throw new Error("City " + city + " not found in tempRegr."); } - if (existsSync("static")) { - console.log(" => static/ already exists."); - } else { - mkdirSync("static"); - - writeFileSync("static/style.css", styleCss); - console.log(" => static/style.css has been created."); - - writeFileSync("static/highlight-theme.css", highlightThemeCss); - console.log(" => static/highlight-theme.css has been created."); - - try { - const res = await fetch("https://svelte.dev/favicon.png"); - const arrayBuffer = await res.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - writeFileSync("static/favicon.png", buffer); - console.log(" => static/favicon.png has been downloaded."); - } catch (e) { - console.error(" => static/favicon.png could not be created."); - console.error(e); - } + const slopPerDecade = cityRegression.slope * 10; - if (example) { - writeFileSync("static/temp.json", tempJSON); - console.log(" => static/temp.json has been created."); - } + return formatNumber(slopPerDecade, { decimals: 3, suffix: "\xB0C" }); +} +`; +var appDTs = `// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} } +} - if (existsSync("src")) { - console.log(" => src/ already exists."); - } else { - mkdirSync("src"); +export {}; +`; +var appHtml = ` + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + +`; + +// src/templates/configs.ts +var viteConfigTs = `import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; - writeFileSync("src/app.html", appHtml); - console.log(" => src/app.html has been created."); +export default defineConfig({ + plugins: [sveltekit()], +}); +`; +var tsconfigJson = `{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} +`; +function getSvelteConfigJs(pages) { + if (pages) { + return `import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import process from "node:process"; - writeFileSync("src/app.d.ts", appDTs); - console.log(" => src/app.d.ts has been created."); +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + paths: { + base: process.argv.includes("dev") ? "" : process.env.BASE_PATH, + }, + }, +}; - mkdirSync("src/routes"); +export default config;`; + } else { + return `import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; - writeFileSync("src/routes/+page.svelte", pageSvelte); - console.log(" => src/routes/+page.svelte has been created."); +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + }, +}; - if (example) { - writeFileSync("src/routes/+page.ts", pageTs); - console.log(" => src/routes/+page.ts has been created."); - } +export default config;`; + } +} +var deployYml = `name: Deploy to GitHub Pages - writeFileSync("src/routes/+layout.ts", layoutTs); - console.log(" => src/routes/+layout.ts has been created."); +on: + push: + branches: "main" - writeFileSync("src/routes/+layout.svelte", layoutSvelte); - console.log(" => src/routes/+layout.svelte has been created."); +jobs: + build_site: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 - mkdirSync("src/lib"); + - name: Install Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x - writeFileSync("src/lib/index.ts", libIndexTs); - console.log(" => src/lib/index.ts has been created."); + - name: Install dependencies + run: deno install - mkdirSync("src/helpers"); - console.log(" => src/helpers/ has been created."); + - name: build + env: + BASE_PATH: "/\${{ github.event.repository.name }}" + run: deno task build - if (example) { - writeFileSync("src/helpers/getTempChange.ts", getTempChangeTs); - console.log(" => src/helpers/getTempChange.ts has been created."); - } + - name: Upload Artifacts + uses: actions/upload-pages-artifact@v3 + with: + path: "build/" - mkdirSync("src/components"); + deploy: + needs: build_site + runs-on: ubuntu-latest - writeFileSync("src/components/Table.svelte", tableSvelte); - console.log(" => src/components/Table.svelte has been created."); + permissions: + pages: write + id-token: write - writeFileSync("src/components/Select.svelte", selectSvelte); - console.log(" => src/components/Select.svelte has been created."); + environment: + name: github-pages + url: \${{ steps.deployment.outputs.page_url }} - writeFileSync("src/components/Radio.svelte", radioSvelte); - console.log(" => src/components/Radio.svelte has been created."); + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 +`; +var svelteGitignore = `node_modules - writeFileSync("src/components/CodeHighlight.svelte", CodeHighlightSvelte); - console.log(" => src/components/CodeHighlight.svelte has been created."); +# Output +.output +.vercel +/.svelte-kit +/build - writeFileSync("src/components/Highlight.svelte", highlightSvelte); - console.log(" => src/components/Highlight.svelte has been created."); +# OS +.DS_Store +Thumbs.db - if (example) { - writeFileSync("src/components/Chart.svelte", chartSvelte); - console.log(" => src/components/Chart.svelte has been created."); - } +# Env +.env +.env.* +!.env.example +!.env.test - mkdirSync("src/data"); - console.log(" => src/data/ has been created."); +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* +# Added by setup-sda +.tmp +.sda-cache +.journalism-cache +sda/data`; +var baseGitignore = `# Added by setup-sda +node_modules +.tmp +.sda-cache +.journalism-cache +sda/data +.env +.DS_Store`; +function getMainTs(example, svelte) { + if (svelte) { if (example) { - writeFileSync("src/data/temp-regressions.json", tempRegressionsJSON); - console.log(" => src/data/temp-regressions.json has been created."); - } - } + return `import { SimpleDB } from "@nshiab/simple-data-analysis"; +import crunchData from "./helpers/crunchData.ts"; - if (existsSync("sda")) { - console.log(" => sda/ already exists."); - } else { - mkdirSync("sda"); +const sdb = new SimpleDB({ logDuration: true }); - writeFileSync("sda/main.ts", mainTs); - console.log(" => sda/main.ts has been created."); +await crunchData(sdb); - mkdirSync("sda/helpers"); - console.log(" => sda/helpers/ has been created."); - - if (example) { - writeFileSync("sda/helpers/crunchData.ts", crunchDataTs); - console.log(" => sda/helpers/crunchData.ts has been created."); - } - - mkdirSync("sda/data"); - console.log(" => sda/data/ has been created."); - - if (example) { - writeFileSync("sda/data/temp.csv", tempCsv); - console.log(" => sda/data/temp.csv has been created."); - } - - mkdirSync("sda/output"); - console.log(" => sda/output/ has been created."); - } - - if (args.includes("--pages")) { - mkdirSync(".github"); - console.log(" => .github/ has been created."); - mkdirSync(".github/workflows"); - console.log(" => .github/workflows/ has been created."); - writeFileSync( - ".github/workflows/deploy.yml", - deployYml, - ); - console.log(" => .github/workflows/deploy.yml has been created."); - } - - if (args.includes("--env")) { - if (existsSync(".env")) { - console.log(" => .env already exists."); +await sdb.done();`; } else { - writeFileSync(".env", ""); - console.log(" => .env has been created."); - } - } - - if (runtime === "nodejs") { - console.log("\n3 - Installing libraries with NPM..."); - - execSync("npm i @sveltejs/adapter-auto --save-dev", { - stdio: "ignore", - }); - console.log(" => @sveltejs/adapter-auto has been installed from NPM."); + return `import { SimpleDB } from "@nshiab/simple-data-analysis"; - execSync("npm i @sveltejs/adapter-static --save-dev", { - stdio: "ignore", - }); - console.log(" => @sveltejs/adapter-static has been installed from NPM."); - - execSync("npm i @sveltejs/kit --save-dev", { - stdio: "ignore", - }); - console.log(" => @sveltejs/kit has been installed from NPM."); - - execSync("npm i @sveltejs/vite-plugin-svelte --save-dev", { - stdio: "ignore", - }); - console.log( - " => @sveltejs/vite-plugin-svelte has been installed from NPM.", - ); - - execSync("npm i svelte --save-dev", { - stdio: "ignore", - }); - console.log(" => svelte has been installed from NPM."); - - execSync("npm i svelte-check --save-dev", { - stdio: "ignore", - }); - console.log(" => svelte-check has been installed from NPM."); - - execSync("npm i typescript --save-dev", { - stdio: "ignore", - }); - console.log(" => typescript has been installed from NPM."); - - execSync("npm i vite --save-dev", { - stdio: "ignore", - }); - console.log(" => vite has been installed from NPM."); - - execSync("npm i highlight.js", { - stdio: "ignore", - }); - console.log(" => highlight.js has been installed from NPM."); - - execSync("npm i @observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); - - execSync("npx jsr add @nshiab/simple-data-analysis", { - stdio: "ignore", - }); - console.log(" => simple-data-analysis has been installed from JSR."); - - execSync("npx jsr add @nshiab/journalism", { - stdio: "ignore", - }); - console.log(" => journalism has been installed from JSR."); - - if (args.includes("--scrape")) { - execSync("npm i cheerio", { - stdio: "ignore", - }); - console.log(" => cheerio has been installed from NPM."); - execSync("npm i playwright-chromium", { - stdio: "ignore", - }); - console.log(" => playwright-chromium has been installed from NPM."); - execSync("npx jsr add @std/fs", { - stdio: "ignore", - }); - console.log(" => @std/fs has been installed from JSR."); - } - } else if (runtime === "bun") { - console.log("\n3 - Installing libraries with Bun..."); - - execSync("bun add @sveltejs/adapter-auto --dev", { - stdio: "ignore", - }); - console.log(" => @sveltejs/adapter-auto has been installed from NPM."); - - execSync("bun add @sveltejs/adapter-static --dev", { - stdio: "ignore", - }); - console.log(" => @sveltejs/adapter-static has been installed from NPM."); - - execSync("bun add @sveltejs/kit --dev", { - stdio: "ignore", - }); - console.log(" => @sveltejs/kit has been installed from NPM."); - - execSync("bun add @sveltejs/vite-plugin-svelte --dev", { - stdio: "ignore", - }); - console.log( - " => @sveltejs/vite-plugin-svelte has been installed from NPM.", - ); - - execSync("bun add svelte --dev", { - stdio: "ignore", - }); - console.log(" => svelte has been installed from NPM."); - - execSync("bun add svelte-check --dev", { - stdio: "ignore", - }); - console.log(" => svelte-check has been installed from NPM."); - - execSync("bun add typescript --dev", { - stdio: "ignore", - }); - console.log(" => typescript has been installed from NPM."); - - execSync("bun add vite --dev", { - stdio: "ignore", - }); - console.log(" => vite has been installed from NPM."); - - execSync("bun add highlight.js", { - stdio: "ignore", - }); - console.log(" => highlight.js has been installed from NPM."); - - execSync("bun add @observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); +const sdb = new SimpleDB(); - execSync("bunx jsr add @nshiab/simple-data-analysis", { - stdio: "ignore", - }); - console.log(" => simple-data-analysis has been installed from JSR."); +// Do your magic here... - execSync("bunx jsr add @nshiab/journalism", { - stdio: "ignore", - }); - console.log(" => journalism has been installed from JSR."); - - if (args.includes("--scrape")) { - execSync("bun add cheerio", { - stdio: "ignore", - }); - console.log(" => cheerio has been installed from NPM."); - execSync("bun add playwright-chromium", { - stdio: "ignore", - }); - console.log(" => playwright-chromium has been installed from NPM."); - execSync("bunx jsr add @std/fs", { - stdio: "ignore", - }); - console.log(" => @std/fs has been installed from JSR."); +await sdb.done();`; } - } else if (runtime === "deno") { - console.log("\n3 - Installing libraries with Deno..."); - - execSync( - "deno install --node-modules-dir=auto --dev --allow-scripts=npm:@sveltejs/kit npm:@sveltejs/adapter-auto", - { - stdio: "ignore", - }, - ); - console.log(" => @sveltejs/adapter-auto has been installed from NPM."); - - execSync( - "deno install --node-modules-dir=auto --dev npm:@sveltejs/adapter-static", - { - stdio: "ignore", - }, - ); - console.log(" => @sveltejs/adapter-static has been installed from NPM."); - - execSync("deno install --node-modules-dir=auto --dev npm:@sveltejs/kit", { - stdio: "ignore", - }); - console.log(" => @sveltejs/kit has been installed from NPM."); + } else { + if (example) { + return `import { SimpleDB } from "@nshiab/simple-data-analysis"; +import crunchData from "./helpers/crunchData.ts"; - execSync( - "deno install --node-modules-dir=auto --dev npm:@sveltejs/vite-plugin-svelte", - { - stdio: "ignore", - }, - ); - console.log( - " => @sveltejs/vite-plugin-svelte has been installed from NPM.", - ); +const sdb = new SimpleDB({ logDuration: true}); - execSync("deno install --node-modules-dir=auto --dev npm:svelte", { - stdio: "ignore", - }); - console.log(" => svelte has been installed from NPM."); +await crunchData(sdb); - execSync("deno install --node-modules-dir=auto --dev npm:svelte-check", { - stdio: "ignore", - }); - console.log(" => svelte-check has been installed from NPM."); +await sdb.done(); - execSync("deno install --node-modules-dir=auto --dev npm:typescript", { - stdio: "ignore", - }); - console.log(" => typescript has been installed from NPM."); +`; + } else { + return `import { SimpleDB } from "@nshiab/simple-data-analysis"; - execSync("deno install --node-modules-dir=auto --dev npm:vite", { - stdio: "ignore", - }); - console.log(" => vite has been installed from NPM."); +const sdb = new SimpleDB(); - execSync("deno install --node-modules-dir=auto npm:highlight.js", { - stdio: "ignore", - }); - console.log(" => highlight.js has been installed from NPM."); +// Do your magic here! - execSync("deno install --node-modules-dir=auto npm:@observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); - - writeFileSync(".npmrc", "@jsr:registry=https://npm.jsr.io"); - const packageJson = { - "type": "module", - "dependencies": { - "@nshiab/journalism": "npm:@jsr/nshiab__journalism@^1.27.7", - "@nshiab/simple-data-analysis": - "npm:@jsr/nshiab__simple-data-analysis@^5.1.7", - }, - }; - writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); - execSync( - "deno install --node-modules-dir=auto --allow-scripts=npm:playwright-chromium", - { - stdio: "ignore", - }, - ); - console.log( - " => simple-data-analysis has been installed from JSR (with a package.json).", - ); - console.log( - " => journalism has been installed from JSR (with a package.json).", - ); - if (args.includes("--scrape")) { - execSync("deno install --node-modules-dir=auto npm:cheerio", { - stdio: "ignore", - }); - console.log(" => cheerio has been installed from NPM."); - execSync( - "deno install --node-modules-dir=auto --allow-scripts=npm:playwright-chromium npm:playwright-chromium", - { - stdio: "ignore", - }, - ); - console.log(" => playwright-chromium has been installed from NPM."); - execSync("deno install --node-modules-dir=auto jsr:@std/fs", { - stdio: "ignore", - }); - console.log(" => @std/fs has been installed from JSR."); +await sdb.done(); +`; } } - - if (args.includes("--git")) { - console.log("\n4 - Initializing Git repository..."); - execSync("git init && git add -A && git commit -m 'Setup done'", { - stdio: "ignore", - }); - console.log(" => First commit done"); - } - - console.log("\nSetup is done!\n"); - - if (runtime === "nodejs") { - console.log(" => Run 'npm run sda' to watch sda/main.ts."); - console.log( - " => Run 'npm run dev' to start a local server.", - ); - } else if (runtime === "bun") { - console.log(" => Run 'bun run sda' to watch sda/main.ts."); - console.log( - " => Run 'bun run dev' to start a local server.", - ); - } else if (runtime === "deno") { - console.log(" => Run 'deno task sda' to watch sda/main.ts."); - console.log( - " => Run 'deno task dev' to start a local server.", - ); - } - - console.log("\nCheck the README.md and have fun. ^_^\n"); -} else if (args.includes("--example")) { - console.log(` => You passed the option --example.`); - const readme = - `This repository has been created with [setup-sda](https://github.com/nshiab/setup-sda/). - -It's using [simple-data-analysis](https://github.com/nshiab/simple-data-analysis) and [journalism](https://github.com/nshiab/journalism). - -Here's the recommended workflow: - -- Put your raw data in the \`sda/data\` folder. Note that this folder is gitignored. -- Use the \`sda/main.ts\` file to clean and process your raw data. Write the results to the \`sda/output\` folder. - -When working on your project, use the following command: - -- \`${ - runtime === "deno" - ? "deno task" - : "npm run" - } sda\` will watch your \`sda/main.ts\` and its dependencies. Everytime you'll save some changes, the data will be reprocessed. -`; - - const tsconfigContent = `{ - "compilerOptions": { - "module": "NodeNext", - "allowImportingTsExtensions": true, - "noEmit": true, - "verbatimModuleSyntax": null - }, - "include": ["**/*"], - "exclude": ["node_modules"] } -`; - - const packageJson = { - type: "module", - scripts: { - sda: `node ${ - args.includes("--env") ? "--env-file=.env " : "" - }--experimental-strip-types --no-warnings --watch sda/main.ts`, - clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp", - }, - }; - - const denoJson = { - tasks: { - sda: `deno run ${ - args.includes("--env") ? "--env-file " : "" - }--node-modules-dir=auto -A --watch --check sda/main.ts`, - clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp", - }, - nodeModulesDir: "auto", - }; - - const mainTs = `import { SimpleDB } from "@nshiab/simple-data-analysis"; -import crunchData from "./helpers/crunchData.ts"; +function getCrunchDataTs(svelte) { + if (svelte) { + return `import type { SimpleDB } from "@nshiab/simple-data-analysis"; -const sdb = new SimpleDB({ logDuration: true}); - -await crunchData(sdb); +export default async function crunchData(sdb: SimpleDB) { -await sdb.done(); + // The mean temperature per decade. + const temp = sdb.newTable("temp"); + await temp.loadData("sda/data/temp.csv"); + await temp.logTable(); -`; + // We compute a linear regression for each city. + const tempRegressions = await temp.linearRegressions({ + x: "decade", + y: "meanTemp", + categories: "city", + decimals: 4, + outputTable: "tempRegressions", + }); + await tempRegressions.logTable(); - const crunchData = - `import type { SimpleDB } from "@nshiab/simple-data-analysis"; + // We write the results to src/data + await tempRegressions.writeData("src/data/temp-regressions.json"); + // Or to static + await temp.writeData("static/temp.json"); +}`; + } else { + return `import type { SimpleDB } from "@nshiab/simple-data-analysis"; export default async function crunchData(sdb: SimpleDB) { @@ -2524,7 +1704,7 @@ export default async function crunchData(sdb: SimpleDB) { await temp.logLineChart("decade", "meanTemp", { smallMultiples: "city", fixedScales: true, - formatY: (d) => \`\${d}°C\`, + formatY: (d) => \\\`\\\${d}\xB0C\\\`, }); // We compute a linear regression for each city. @@ -2541,275 +1721,9 @@ export default async function crunchData(sdb: SimpleDB) { await tempRegressions.writeData("sda/output/temp-regressions.json"); } `; - - const data = `city,decade,meanTemp -Toronto,1840.0,7.1 -Toronto,1850.0,6.4 -Toronto,1860.0,6.9 -Toronto,1870.0,6.8 -Montreal,1880.0,5.3 -Toronto,1880.0,6.6 -Montreal,1890.0,6.0 -Toronto,1890.0,7.6 -Montreal,1900.0,5.8 -Toronto,1900.0,7.7 -Montreal,1910.0,6.1 -Toronto,1910.0,8.1 -Montreal,1920.0,6.2 -Toronto,1920.0,8.0 -Montreal,1930.0,6.8 -Toronto,1930.0,8.6 -Montreal,1940.0,6.9 -Toronto,1940.0,8.7 -Montreal,1950.0,7.4 -Toronto,1950.0,9.2 -Montreal,1960.0,7.4 -Toronto,1960.0,8.8 -Montreal,1970.0,7.3 -Toronto,1970.0,9.0 -Montreal,1980.0,7.7 -Toronto,1980.0,9.0 -Toronto,1990.0,9.6 -Montreal,2000.0,7.6 -Toronto,2000.0,9.7 -Montreal,2010.0,8.0 -Toronto,2010.0,9.9 -`; - - console.log("\n2 - Creating relevant files..."); - - if (runtime === "nodejs") { - if (existsSync("package.json")) { - console.log(" => package.json already exists."); - const currentPackageJson = JSON.parse( - readFileSync("package.json", "utf-8"), - ); - currentPackageJson.scripts = { - ...currentPackageJson.scripts, - ...packageJson.scripts, - }; - writeFileSync( - "package.json", - JSON.stringify(currentPackageJson, null, 2), - ); - console.log(" => package.json has been modified."); - } else { - writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); - console.log(" => package.json has been created."); - } - - if (existsSync("tsconfig.json")) { - console.log(" => tsconfig.json already exists."); - } else { - writeFileSync("tsconfig.json", tsconfigContent); - console.log(" => tsconfig.json has been created."); - } - } else if (runtime === "bun") { - packageJson.scripts = { - sda: "bun --watch sda/main.ts", - }; - - if (existsSync("package.json")) { - console.log(" => package.json already exists."); - const currentPackageJson = JSON.parse( - readFileSync("package.json", "utf-8"), - ); - currentPackageJson.scripts = { - ...currentPackageJson.scripts, - ...packageJson.scripts, - }; - writeFileSync( - "package.json", - JSON.stringify(currentPackageJson, null, 2), - ); - console.log(" => package.json has been modified."); - } else { - writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); - console.log(" => package.json has been created."); - } - - if (existsSync("tsconfig.json")) { - console.log(" => tsconfig.json already exists."); - } else { - writeFileSync("tsconfig.json", tsconfigContent); - console.log(" => tsconfig.json has been created."); - } - } else if (runtime === "deno") { - if (existsSync("deno.json")) { - console.log(" => deno.json already exists."); - const currentDenoJson = JSON.parse( - readFileSync("deno.json", "utf-8"), - ); - currentDenoJson.tasks = { - ...currentDenoJson.tasks, - ...denoJson.tasks, - }; - writeFileSync("deno.json", JSON.stringify(currentDenoJson, null, 2)); - console.log(" => deno.json has been modified."); - } else { - writeFileSync("deno.json", JSON.stringify(denoJson, null, 2)); - console.log(" => deno.json has been created."); - } - } - - if (existsSync(".gitignore")) { - console.log(" => .gitignore already exists. Appending to it."); - const currentGitignore = readFileSync(".gitignore", "utf-8"); - writeFileSync( - ".gitignore", - currentGitignore + "\n" + - "\n# Added by setup-sda\nnode_modules\n.tmp\n.sda-cache\n.journalism-cache\nsda/data\n.env\n.DS_Store", - ); - } else { - writeFileSync( - ".gitignore", - "\n# Added by setup-sda\nnode_modules\n.tmp\n.sda-cache\n.journalism-cache\nsda/data\n.env\n.DS_Store", - ); - console.log(" => .gitignore has been created."); } - - if (existsSync("sda")) { - console.log(" => sda/ already exists."); - } else { - mkdirSync("sda"); - writeFileSync( - "sda/main.ts", - mainTs, - ); - console.log(" => sda/main.ts has been created."); - - mkdirSync("sda/helpers"); - writeFileSync( - "sda/helpers/crunchData.ts", - crunchData, - ); - console.log(" => sda/helpers/crunchData.ts has been created."); - - mkdirSync("sda/data"); - writeFileSync("sda/data/temp.csv", data); - console.log(" => sda/data/temp.csv has been created."); - mkdirSync("sda/output"); - console.log(" => sda/output/ has been created."); - } - - if (existsSync("README.md")) { - console.log(" => README.md already exists."); - } else { - writeFileSync("README.md", readme); - console.log(" => README.md has been created."); - } - - if (args.includes("--pages")) { - mkdirSync(".github"); - console.log(" => .github/ has been created."); - mkdirSync(".github/workflows"); - console.log(" => .github/workflows/ has been created."); - writeFileSync( - ".github/workflows/deploy.yml", - deployYml, - ); - console.log(" => .github/workflows/deploy.yml has been created."); - } - - if (args.includes("--env")) { - if (existsSync(".env")) { - console.log(" => .env already exists."); - } else { - writeFileSync(".env", ""); - console.log(" => .env has been created."); - } - } - - if (runtime === "nodejs") { - console.log("\n3 - Installing libraries..."); - execSync("npx jsr add @nshiab/simple-data-analysis", { - stdio: "ignore", - }); - console.log(" => simple-data-analysis has been installed from JSR."); - execSync("npx jsr add @nshiab/journalism", { - stdio: "ignore", - }); - console.log(" => journalism has been installed from JSR."); - execSync("npm i @observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); - } else if (runtime === "bun") { - console.log("\n3 - Installing libraries with Bun..."); - execSync("bunx jsr add @nshiab/simple-data-analysis", { - stdio: "ignore", - }); - console.log(" => simple-data-analysis has been installed from JSR."); - execSync("bunx jsr add @nshiab/journalism", { - stdio: "ignore", - }); - console.log(" => journalism has been installed from JSR."); - execSync("bun add @observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); - } else if (runtime === "deno") { - console.log("\n3 - Installing libraries with Deno..."); - execSync( - "deno install --node-modules-dir=auto --allow-scripts=npm:playwright-chromium jsr:@nshiab/simple-data-analysis", - { - stdio: "ignore", - }, - ); - console.log(" => simple-data-analysis has been installed from JSR."); - execSync( - "deno install --node-modules-dir=auto --allow-scripts=npm:playwright-chromium jsr:@nshiab/journalism", - { - stdio: "ignore", - }, - ); - console.log(" => journalism has been installed JSR."); - execSync("deno install --node-modules-dir=auto npm:@observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); - } - - if (args.includes("--git")) { - console.log("\n4 - Initializing Git repository..."); - execSync("git init && git add -A && git commit -m 'Setup done'", { - stdio: "ignore", - }); - console.log(" => First commit done"); - } - - console.log("\nSetup is done!\n"); - - if (runtime === "nodejs") { - console.log(" => Run 'npm run sda' to watch main.ts."); - } else if (runtime === "bun") { - console.log(" => Run 'bun run sda' to watch main.ts."); - } else if (runtime === "deno") { - console.log(" => Run 'deno task sda' to watch main.ts."); - } - - console.log("\nCheck the README.md and have fun. ^_^\n"); -} else { - const readme = - `This repository has been created with [setup-sda](https://github.com/nshiab/setup-sda/). - -It's using [simple-data-analysis](https://github.com/nshiab/simple-data-analysis) and [journalism](https://github.com/nshiab/journalism). - -Here's the recommended workflow: - -- Put your raw data in the \`sda/data\` folder. Note that this folder is gitignored. -- Use the \`sda/main.ts\` file to clean and process your raw data. Write the results to the \`sda/output\` folder. - -When working on your project, use the following command: - -- \`${ - runtime === "deno" - ? "deno task" - : "npm run" - } sda\` will watch your \`sda/main.ts\` and its dependencies. Everytime you'll save some changes, the data will be reprocessed. -`; - - const tsconfigContent = `{ +} +var baseTsconfigJson = `{ "compilerOptions": { "module": "NodeNext", "allowImportingTsExtensions": true, @@ -2821,282 +1735,360 @@ When working on your project, use the following command: } `; - const packageJson = { - type: "module", - scripts: { - sda: `node ${ - args.includes("--env") ? "--env-file=.env " : "" - }--experimental-strip-types --no-warnings --watch sda/main.ts`, - clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp", - }, - }; - - const denoJson = { - tasks: { - sda: `deno run ${ - args.includes("--env") ? "--env-file " : "" - }--node-modules-dir=auto -A --watch --check sda/main.ts`, - clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp", - }, - nodeModulesDir: "auto", - }; - - const mainTs = `import { SimpleDB } from "@nshiab/simple-data-analysis"; - -const sdb = new SimpleDB(); - -// Do your magic here! - -await sdb.done(); -`; - +// src/actions/svelte.ts +async function setupSvelte(args, runtime, execSync) { + if (!args.includes("--svelte")) return; + const example = args.includes("--example"); console.log("\n2 - Creating relevant files..."); - if (runtime === "nodejs") { - if (existsSync("package.json")) { - console.log(" => package.json already exists."); - const currentPackageJson = JSON.parse( - readFileSync("package.json", "utf-8"), - ); - currentPackageJson.scripts = { - ...currentPackageJson.scripts, - ...packageJson.scripts, + if (existsSync2("package.json")) { + const current = JSON.parse(readFileSync("package.json", "utf-8")); + current.scripts = { + ...current.scripts, + dev: "vite dev", + build: "vite build", + preview: "vite preview", + check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "node " + (args.includes("--env") ? "--env-file=.env " : "") + "--experimental-strip-types --no-warnings --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" }; - writeFileSync( - "package.json", - JSON.stringify(currentPackageJson, null, 2), - ); - console.log(" => package.json has been modified."); + writeFileSync2("package.json", JSON.stringify(current, null, 2)); } else { - writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); - console.log(" => package.json has been created."); - } - - if (existsSync("tsconfig.json")) { - console.log(" => tsconfig.json already exists."); - } else { - writeFileSync("tsconfig.json", tsconfigContent); - console.log(" => tsconfig.json has been created."); + const pkg = { + type: "module", + scripts: { + dev: "vite dev", + build: "vite build", + preview: "vite preview", + check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "node " + (args.includes("--env") ? "--env-file=.env " : "") + "--experimental-strip-types --no-warnings --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + } + }; + writeFileSync2("package.json", JSON.stringify(pkg, null, 2)); } + if (!existsSync2("tsconfig.json")) writeFileSync2("tsconfig.json", tsconfigJson); + if (!existsSync2("vite.config.ts")) writeFileSync2("vite.config.ts", viteConfigTs); + if (!existsSync2("svelte.config.js")) writeFileSync2("svelte.config.js", getSvelteConfigJs(args.includes("--pages"))); } else if (runtime === "bun") { - packageJson.scripts = { - sda: "bun --watch sda/main.ts", - }; - - if (existsSync("package.json")) { - console.log(" => package.json already exists."); - const currentPackageJson = JSON.parse( - readFileSync("package.json", "utf-8"), - ); - currentPackageJson.scripts = { - ...currentPackageJson.scripts, - ...packageJson.scripts, + if (existsSync2("package.json")) { + const current = JSON.parse(readFileSync("package.json", "utf-8")); + current.scripts = { + ...current.scripts, + dev: "vite dev", + build: "vite build", + preview: "vite preview", + check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "bun --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" }; - writeFileSync( - "package.json", - JSON.stringify(currentPackageJson, null, 2), - ); - console.log(" => package.json has been modified."); - } else { - writeFileSync("package.json", JSON.stringify(packageJson, null, 2)); - console.log(" => package.json has been created."); - } - - if (existsSync("tsconfig.json")) { - console.log(" => tsconfig.json already exists."); + writeFileSync2("package.json", JSON.stringify(current, null, 2)); } else { - writeFileSync("tsconfig.json", tsconfigContent); - console.log(" => tsconfig.json has been created."); + const pkg = { + type: "module", + scripts: { + dev: "vite dev", + build: "vite build", + preview: "vite preview", + check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "bun --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + } + }; + writeFileSync2("package.json", JSON.stringify(pkg, null, 2)); } + if (!existsSync2("tsconfig.json")) writeFileSync2("tsconfig.json", tsconfigJson); + if (!existsSync2("vite.config.ts")) writeFileSync2("vite.config.ts", viteConfigTs); + if (!existsSync2("svelte.config.js")) writeFileSync2("svelte.config.js", getSvelteConfigJs(args.includes("--pages"))); } else if (runtime === "deno") { - if (existsSync("deno.json")) { - console.log(" => deno.json already exists."); - const currentDenoJson = JSON.parse( - readFileSync("deno.json", "utf-8"), - ); - currentDenoJson.tasks = { - ...currentDenoJson.tasks, - ...denoJson.tasks, + const denoJson = { + tasks: { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "deno run " + (args.includes("--env") ? "--env-file " : "") + "--node-modules-dir=auto -A --watch --check sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + }, + nodeModulesDir: "auto", + "unstable": [ + "sloppy-imports", + "fmt-component" + ], + "lint": { + "rules": { + "exclude": [ + "no-sloppy-imports" + ] + } + }, + "imports": { + "$lib": "./src/lib/index.ts", + "$lib/": "./src/lib/" + }, + "compilerOptions": { + "rootDirs": [ + ".", + "./.svelte-kit/types" + ], + "verbatimModuleSyntax": true, + "lib": [ + "esnext", + "DOM", + "DOM.Iterable", + "deno.ns" + ], + "types": [ + "./.svelte-kit/ambient.d.ts", + "./.svelte-kit/non-ambient.d.ts" + ] + } + }; + if (existsSync2("deno.json")) { + const current = JSON.parse(readFileSync("deno.json", "utf-8")); + current.tasks = { + ...current.tasks, + ...denoJson.tasks }; - writeFileSync("deno.json", JSON.stringify(currentDenoJson, null, 2)); - console.log(" => deno.json has been modified."); + writeFileSync2("deno.json", JSON.stringify(current, null, 2)); } else { - writeFileSync("deno.json", JSON.stringify(denoJson, null, 2)); - console.log(" => deno.json has been created."); + writeFileSync2("deno.json", JSON.stringify(denoJson, null, 2)); } + if (!existsSync2("tsconfig.json")) writeFileSync2("tsconfig.json", tsconfigJson); + if (!existsSync2("vite.config.ts")) writeFileSync2("vite.config.ts", viteConfigTs); + if (!existsSync2("svelte.config.js")) writeFileSync2("svelte.config.js", getSvelteConfigJs(args.includes("--pages"))); } - - if (existsSync(".gitignore")) { - console.log(" => .gitignore already exists. Appending to it."); - const currentGitignore = readFileSync(".gitignore", "utf-8"); - writeFileSync( - ".gitignore", - currentGitignore + "\n" + - "\n# Added by setup-sda\nnode_modules\n.tmp\n.sda-cache\n.journalism-cache\nsda/data\n.env\n.DS_Store", - ); + if (existsSync2(".gitignore")) { + const current = readFileSync(".gitignore", "utf-8"); + writeFileSync2(".gitignore", current + "\n\n# Added by setup-sda\n" + svelteGitignore); } else { - writeFileSync( - ".gitignore", - "# Added by setup-sda\nnode_modules\n.tmp\n.sda-cache\n.journalism-cache\nsda/data\n.env\n.DS_Store", - ); - console.log(" => .gitignore has been created."); + writeFileSync2(".gitignore", svelteGitignore); } - - if (existsSync("sda")) { - console.log(" => sda/ already exists."); - } else { - mkdirSync("sda"); - writeFileSync( - "sda/main.ts", - mainTs, - ); - console.log(" => sda/main.ts has been created."); - - mkdirSync("sda/helpers"); - console.log(" => sda/helpers/ has been created."); - - mkdirSync("sda/data"); - console.log(" => sda/data/ has been created."); - mkdirSync("sda/output"); - console.log(" => sda/output/ has been created."); + if (!existsSync2(".svelte-kit")) mkdirSync2(".svelte-kit", { + recursive: true + }); + writeFileSync2(".svelte-kit/ambient.d.ts", ""); + writeFileSync2(".svelte-kit/non-ambient.d.ts", ""); + if (!existsSync2("static")) mkdirSync2("static", { + recursive: true + }); + writeFileSync2("static/style.css", styleCss); + writeFileSync2("static/highlight-theme.css", highlightThemeCss); + try { + const res = await fetch("https://svelte.dev/favicon.png"); + const arrayBuffer = await res.arrayBuffer(); + writeFileSync2("static/favicon.png", Buffer.from(arrayBuffer)); + } catch (e) { + console.error(" => static/favicon.png could not be created."); } - - if (existsSync("README.md")) { - console.log(" => README.md already exists."); - } else { - writeFileSync("README.md", readme); - console.log(" => README.md has been created."); + if (example) writeFileSync2("static/temp.json", JSON.stringify([])); + if (!existsSync2("src")) mkdirSync2("src", { + recursive: true + }); + writeFileSync2("src/app.html", appHtml); + writeFileSync2("src/app.d.ts", appDTs); + if (!existsSync2("src/routes")) mkdirSync2("src/routes", { + recursive: true + }); + writeFileSync2("src/routes/+page.svelte", getPageSvelte(example)); + if (example) writeFileSync2("src/routes/+page.ts", pageTs); + writeFileSync2("src/routes/+layout.ts", layoutTs); + writeFileSync2("src/routes/+layout.svelte", getLayoutSvelte(example, args.includes("--pages"))); + if (!existsSync2("src/lib")) mkdirSync2("src/lib", { + recursive: true + }); + writeFileSync2("src/lib/index.ts", getLibIndexTs(example)); + if (!existsSync2("src/helpers")) mkdirSync2("src/helpers", { + recursive: true + }); + if (example) writeFileSync2("src/helpers/getTempChange.ts", getTempChangeTs); + if (!existsSync2("src/components")) mkdirSync2("src/components", { + recursive: true + }); + writeFileSync2("src/components/Table.svelte", tableSvelte); + writeFileSync2("src/components/Select.svelte", selectSvelte); + writeFileSync2("src/components/Radio.svelte", radioSvelte); + writeFileSync2("src/components/CodeHighlight.svelte", CodeHighlightSvelte); + writeFileSync2("src/components/Highlight.svelte", highlightSvelte); + if (example) writeFileSync2("src/components/Chart.svelte", chartSvelte); + if (!existsSync2("src/data")) mkdirSync2("src/data", { + recursive: true + }); + if (!existsSync2("sda")) mkdirSync2("sda", { + recursive: true + }); + writeFileSync2("sda/main.ts", getMainTs(example, true)); + if (!existsSync2("sda/helpers")) mkdirSync2("sda/helpers", { + recursive: true + }); + if (example) writeFileSync2("sda/helpers/crunchData.ts", getCrunchDataTs(true)); + if (!existsSync2("sda/data")) mkdirSync2("sda/data", { + recursive: true + }); + if (example) writeFileSync2("sda/data/temp.csv", "city,decade,meanTemp\nToronto,1840.0,7.1"); + if (!existsSync2("sda/output")) mkdirSync2("sda/output", { + recursive: true + }); + if (args.includes("--pages")) { + if (!existsSync2(".github")) mkdirSync2(".github", { + recursive: true + }); + if (!existsSync2(".github/workflows")) mkdirSync2(".github/workflows", { + recursive: true + }); + writeFileSync2(".github/workflows/deploy.yml", deployYml); } - - if (args.includes("--env")) { - if (existsSync(".env")) { - console.log(" => .env already exists."); - } else { - writeFileSync(".env", ""); - console.log(" => .env has been created."); - } + if (args.includes("--env") && !existsSync2(".env")) writeFileSync2(".env", ""); + const installCmds = { + nodejs: [ + "npm i @sveltejs/adapter-auto @sveltejs/adapter-static @sveltejs/kit @sveltejs/vite-plugin-svelte svelte svelte-check typescript vite highlight.js @observablehq/plot --save-dev", + "npx jsr add @nshiab/simple-data-analysis @nshiab/journalism" + ], + bun: [ + "bun add @sveltejs/adapter-auto @sveltejs/adapter-static @sveltejs/kit @sveltejs/vite-plugin-svelte svelte svelte-check typescript vite highlight.js @observablehq/plot --dev", + "bunx jsr add @nshiab/simple-data-analysis @nshiab/journalism" + ], + deno: [ + "deno install --node-modules-dir=auto --dev --allow-scripts=npm:@sveltejs/kit npm:@sveltejs/adapter-auto npm:@sveltejs/adapter-static npm:@sveltejs/kit npm:@sveltejs/vite-plugin-svelte npm:svelte npm:svelte-check npm:typescript npm:vite npm:highlight.js npm:@observablehq/plot jsr:@nshiab/simple-data-analysis jsr:@nshiab/journalism" + ] + }; + if (args.includes("--scrape")) { + installCmds.nodejs.push("npm i cheerio playwright-chromium"); + installCmds.nodejs.push("npx jsr add @std/fs"); + installCmds.bun.push("bun add cheerio playwright-chromium"); + installCmds.bun.push("bunx jsr add @std/fs"); + installCmds.deno.push("deno install --node-modules-dir=auto npm:cheerio npm:playwright-chromium jsr:@std/fs"); } + console.log("\n3 - Installing libraries with " + (runtime === "nodejs" ? "NPM" : runtime === "bun" ? "Bun" : "Deno") + "..."); + installCmds[runtime].forEach((cmd) => execSync(cmd)); +} - if (runtime === "nodejs") { - console.log("\n3 - Installing libraries..."); - execSync("npx jsr add @nshiab/simple-data-analysis", { - stdio: "ignore", - }); - console.log(" => simple-data-analysis has been installed from JSR."); - execSync("npx jsr add @nshiab/journalism", { - stdio: "ignore", - }); - console.log(" => journalism has been installed from JSR."); - execSync("npm i @observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); - - if (args.includes("--scrape")) { - execSync("npm i cheerio", { - stdio: "ignore", - }); - console.log(" => cheerio has been installed from NPM."); - execSync("npm i playwright-chromium", { - stdio: "ignore", - }); - console.log(" => playwright-chromium has been installed from NPM."); - execSync("npx jsr add @std/fs", { - stdio: "ignore", - }); - console.log(" => @std/fs has been installed from JSR."); - } - } else if (runtime === "bun") { - console.log("\n3 - Installing libraries with Bun..."); - execSync("bunx jsr add @nshiab/simple-data-analysis", { - stdio: "ignore", - }); - console.log(" => simple-data-analysis has been installed from JSR."); - execSync("bunx jsr add @nshiab/journalism", { - stdio: "ignore", - }); - console.log(" => journalism has been installed from JSR."); - execSync("bun add @observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); - - if (args.includes("--scrape")) { - execSync("bun add cheerio", { - stdio: "ignore", - }); - console.log(" => cheerio has been installed from NPM."); - execSync("bun add playwright-chromium", { - stdio: "ignore", - }); - console.log(" => playwright-chromium has been installed from NPM."); - execSync("bunx jsr add @std/fs", { - stdio: "ignore", - }); - console.log(" => @std/fs has been installed from JSR."); - } - } else if (runtime === "deno") { - console.log("\n3 - Installing libraries with Deno..."); - execSync( - "deno install --node-modules-dir=auto --allow-scripts=npm:playwright-chromium jsr:@nshiab/simple-data-analysis", - { - stdio: "ignore", - }, - ); - console.log(" => simple-data-analysis has been installed from JSR."); - execSync( - "deno install --node-modules-dir=auto --allow-scripts=npm:playwright-chromium jsr:@nshiab/journalism", - { - stdio: "ignore", +// src/actions/base.ts +import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync3 } from "node:fs"; +async function setupBase(args, runtime, execSync) { + if (args.includes("--svelte")) return; + const example = args.includes("--example"); + const readme = "This repository has been created with [setup-sda](https://github.com/nshiab/setup-sda/).\n\nIt's using [simple-data-analysis](https://github.com/nshiab/simple-data-analysis) and [journalism](https://github.com/nshiab/journalism).\n\nHere's the recommended workflow:\n\n- Put your raw data in the `sda/data` folder. Note that this folder is gitignored.\n- Use the `sda/main.ts` file to clean and process your raw data. Write the results to the `sda/output` folder.\n\nWhen working on your project, use the following command:\n\n- `" + (runtime === "deno" ? "deno task" : "npm run") + " sda` will watch your `sda/main.ts` and its dependencies. Everytime you'll save some changes, the data will be reprocessed.\n"; + console.log("\n2 - Creating relevant files..."); + if (runtime === "nodejs" || runtime === "bun") { + const pkg = { + type: "module", + scripts: { + sda: "node " + (args.includes("--env") ? "--env-file=.env " : "") + "--experimental-strip-types --no-warnings --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + } + }; + if (runtime === "bun") pkg.scripts.sda = "bun --watch sda/main.ts"; + writeFileSync3("package.json", JSON.stringify(pkg, null, 2)); + writeFileSync3("tsconfig.json", baseTsconfigJson); + } else { + const deno = { + tasks: { + sda: "deno run " + (args.includes("--env") ? "--env-file " : "") + "--node-modules-dir=auto -A --watch --check sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" }, - ); - console.log(" => journalism has been installed from JSR."); - execSync("deno install --node-modules-dir=auto npm:@observablehq/plot", { - stdio: "ignore", - }); - console.log(" => @observablehq/plot has been installed from NPM."); - if (args.includes("--scrape")) { - execSync("deno install --node-modules-dir=auto npm:cheerio", { - stdio: "ignore", - }); - console.log(" => cheerio has been installed from NPM."); - execSync( - "deno install --node-modules-dir=auto --allow-scripts=npm:playwright-chromium npm:playwright-chromium", - { - stdio: "ignore", - }, - ); - console.log(" => playwright-chromium has been installed from NPM."); - execSync("deno install --node-modules-dir=auto jsr:@std/fs", { - stdio: "ignore", - }); - console.log(" => @std/fs has been installed from JSR."); - } + nodeModulesDir: "auto" + }; + writeFileSync3("deno.json", JSON.stringify(deno, null, 2)); } - + if (existsSync3(".gitignore")) { + const current = readFileSync2(".gitignore", "utf-8"); + writeFileSync3(".gitignore", current + "\n" + baseGitignore); + } else { + writeFileSync3(".gitignore", baseGitignore); + } + if (!existsSync3("sda")) mkdirSync3("sda", { + recursive: true + }); + writeFileSync3("sda/main.ts", getMainTs(example, false)); + if (!existsSync3("sda/helpers")) mkdirSync3("sda/helpers", { + recursive: true + }); + if (example) writeFileSync3("sda/helpers/crunchData.ts", getCrunchDataTs(false)); + if (!existsSync3("sda/data")) mkdirSync3("sda/data", { + recursive: true + }); + if (example) writeFileSync3("sda/data/temp.csv", "city,decade,meanTemp\nToronto,1840.0,7.1"); + if (!existsSync3("sda/output")) mkdirSync3("sda/output", { + recursive: true + }); + if (!existsSync3("README.md")) writeFileSync3("README.md", readme); + if (args.includes("--env") && !existsSync3(".env")) writeFileSync3(".env", ""); + const installCmds = { + nodejs: [ + "npx jsr add @nshiab/simple-data-analysis @nshiab/journalism", + "npm i @observablehq/plot" + ], + bun: [ + "bunx jsr add @nshiab/simple-data-analysis @nshiab/journalism", + "bun add @observablehq/plot" + ], + deno: [ + "deno install --node-modules-dir=auto jsr:@nshiab/simple-data-analysis jsr:@nshiab/journalism npm:@observablehq/plot" + ] + }; + if (args.includes("--scrape")) { + installCmds.nodejs.push("npm i cheerio playwright-chromium"); + installCmds.nodejs.push("npx jsr add @std/fs"); + installCmds.bun.push("bun add cheerio playwright-chromium"); + installCmds.bun.push("bunx jsr add @std/fs"); + installCmds.deno.push("deno install --node-modules-dir=auto npm:cheerio npm:playwright-chromium jsr:@std/fs"); + } + console.log("\n3 - Installing libraries..."); + installCmds[runtime].forEach((cmd) => execSync(cmd)); if (args.includes("--git")) { console.log("\n4 - Initializing Git repository..."); - execSync("git init && git add -A && git commit -m 'Setup done'", { - stdio: "ignore", - }); - console.log(" => First commit done"); + execSync("git init && git add -A && git commit -m 'Setup done'"); } - console.log("\nSetup is done!\n"); - - if (runtime === "nodejs") { - console.log(" => Run 'npm run sda' to watch main.ts."); - } else if (runtime === "bun") { - console.log(" => Run 'bun run sda' to watch main.ts."); - } else if (runtime === "deno") { - console.log(" => Run 'deno task sda' to watch main.ts."); - } - - console.log("\nCheck the README.md and have fun. ^_^\n"); } -if (args.includes("--pages") && args.includes("--svelte")) { - console.log( - "PS: Don't forget to enable GitHub Pages in your repository settings.\n", - ); +// src/main.ts +async function main() { + const args = process.argv.slice(2); + const isTest = args.includes("--test"); + const runtime = getRuntime(); + const execSync = createExecSync(isTest); + console.log("\nStarting sda setup..."); + console.log("\n1 - Checking runtime and options..."); + console.log(` => Your navigator userAgent is: ${navigator.userAgent}`); + const flags = [ + "--test", + "--git", + "--scrape", + "--pages", + "--env", + "--claude", + "--gemini", + "--copilot", + "--agent", + "--svelte", + "--example" + ]; + flags.forEach((flag) => { + if (args.includes(flag)) { + console.log(` => You passed the option ${flag}`); + } + }); + if (args.includes("--pages") && !args.includes("--svelte")) { + console.log(` => You passed the option --pages, but it only works with --svelte`); + } + await setupAgents(args); + if (args.includes("--svelte")) { + await setupSvelte(args, runtime, execSync); + } else { + await setupBase(args, runtime, execSync); + } + if (args.includes("--pages") && args.includes("--svelte")) { + console.log("PS: Don't forget to enable GitHub Pages in your repository settings.\n"); + } } +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/actions/agents.ts b/src/actions/agents.ts new file mode 100644 index 0000000..5b5c055 --- /dev/null +++ b/src/actions/agents.ts @@ -0,0 +1,93 @@ +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; + +export async function setupAgents(args: string[]) { + if ( + !args.includes("--claude") && !args.includes("--gemini") && + !args.includes("--copilot") && !args.includes("--agent") + ) return; + + console.log(" => Fetching up-to-date documentation for journalism..."); + const journalismDoc = await fetch( + "https://raw.githubusercontent.com/nshiab/journalism/refs/heads/main/llm.md", + ).then((d) => d.text()); + + const journalismFunctions = journalismDoc + .split("\n") + .filter((line) => line.startsWith("## ")) + .map((line) => line.replace("## ", "").trim()) + .join("\n"); + + console.log(" => Fetching up-to-date documentation for simple-data-analysis..."); + const sdaDoc = await fetch( + "https://raw.githubusercontent.com/nshiab/simple-data-analysis/refs/heads/main/llm.md", + ).then((d) => d.text()); + + const sdaClassesAndMethods = sdaDoc + .split("\n") + .filter((line) => line.startsWith("## ") || line.startsWith("#### ")) + .map((line) => + line.startsWith("## ") + ? "\n" + line.replace("## ", "").trim() + : line.replace("#### Parameters", " Methods:").replace("#### ", " ") + .replaceAll("`", "") + ) + .join("\n"); + + const llm = `Always verify if there is a deno.json or package.json file in the root of the project and familiarize yourself with the scripts available in it and the libraries already installed in the project. + +If it's a Deno project, always run \`deno run -A --node-modules-dir=auto --env-file --check sda/main.ts\` to test your code. Before handing off your work, always run \`deno lint\` and \`deno fmt\` as well. Fix any errors or warnings triggered along the way. + +If it's a Node.js project, always run \`node --env-file=.env --experimental-strip-types --no-warnings sda/main.ts\` to test your code. Before handing off your work, always fix any errors or warnings triggered along the way. + +Always use "sda/main.ts" as the entry point. + +If you need to create other TypeScript files, create them in the "sda/helpers" folder. Prioritize the use of helper functions to keep the code well organized and maintainable, with one helper function per file, with the file named after the function. Prioritize default exports for helper functions. + +If you need to download data, always put the files in the "sda/data" folder, which is gitignored. + +If you need to output data to a file, always put the file in the "sda/output" folder. + +Always prioritize the use of the "journalism" and "simple-data-analysis" libraries. These libraries are already installed in the project and can be used directly like this: +\x60\x60\x60typescript +import { formatDate } from "@nshiab/journalism"; +import { SimpleDB } from "@nshiab/simple-data-analysis"; +\x60\x60\x60 + +Here are the functions available in the "journalism" library. If one of the function might be relevant, read the complete documentation at "./docs/journalism.md" to properly use it. + +\x24{journalismFunctions} + +Here are the classes and their methods available in the "simple-data-analysis" library. If one of the classes or methods might be relevant, read the complete documentation at "./docs/simple-data-analysis.md" to properly use it. Remember that almost all methods are asynchronous, so you need to use \`await\` when calling them. +\x24{sdaClassesAndMethods}`; + + const agentFiles = [ + { flag: "--claude", file: "CLAUDE.md" }, + { flag: "--gemini", file: "GEMINI.md" }, + { flag: "--agent", file: "AGENTS.md" }, + ]; + + for (const { flag, file } of agentFiles) { + if (args.includes(flag)) { + if (existsSync(file)) { + console.log(" => " + file + " already exists. Skipping creation."); + } else { + console.log(" => Creating " + file + "."); + writeFileSync(file, llm); + } + } + } + + if (args.includes("--copilot")) { + if (!existsSync(".github")) mkdirSync(".github", { recursive: true }); + if (existsSync(".github/copilot-instructions.md")) { + console.log(" => .github/copilot-instructions.md already exists. Skipping creation."); + } else { + console.log(" => Creating .github/copilot-instructions.md."); + writeFileSync(".github/copilot-instructions.md", llm); + } + } + + if (!existsSync("docs")) mkdirSync("docs", { recursive: true }); + writeFileSync("docs/journalism.md", journalismDoc); + writeFileSync("docs/simple-data-analysis.md", sdaDoc); +} diff --git a/src/actions/base.ts b/src/actions/base.ts new file mode 100644 index 0000000..ceb65ef --- /dev/null +++ b/src/actions/base.ts @@ -0,0 +1,80 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { Runtime } from "../lib/utils.ts"; +import { baseGitignore, baseTsconfigJson, getMainTs, getCrunchDataTs } from "../templates/configs.ts"; + +export async function setupBase(args: string[], runtime: Runtime, execSync: any) { + if (args.includes("--svelte")) return; + + const example = args.includes("--example"); + + const readme = "This repository has been created with [setup-sda](https://github.com/nshiab/setup-sda/).\n\nIt's using [simple-data-analysis](https://github.com/nshiab/simple-data-analysis) and [journalism](https://github.com/nshiab/journalism).\n\nHere's the recommended workflow:\n\n- Put your raw data in the `sda/data` folder. Note that this folder is gitignored.\n- Use the `sda/main.ts` file to clean and process your raw data. Write the results to the `sda/output` folder.\n\nWhen working on your project, use the following command:\n\n- `" + (runtime === "deno" ? "deno task" : "npm run") + " sda` will watch your `sda/main.ts` and its dependencies. Everytime you'll save some changes, the data will be reprocessed.\n"; + + console.log("\n2 - Creating relevant files..."); + + if (runtime === "nodejs" || runtime === "bun") { + const pkg = { type: "module", scripts: { + sda: "node " + (args.includes("--env") ? "--env-file=.env " : "") + "--experimental-strip-types --no-warnings --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + }}; + if (runtime === "bun") pkg.scripts.sda = "bun --watch sda/main.ts"; + writeFileSync("package.json", JSON.stringify(pkg, null, 2)); + writeFileSync("tsconfig.json", baseTsconfigJson); + } else { + const deno = { tasks: { + sda: "deno run " + (args.includes("--env") ? "--env-file " : "") + "--node-modules-dir=auto -A --watch --check sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + }, nodeModulesDir: "auto" }; + writeFileSync("deno.json", JSON.stringify(deno, null, 2)); + } + + if (existsSync(".gitignore")) { + const current = readFileSync(".gitignore", "utf-8"); + writeFileSync(".gitignore", current + "\n" + baseGitignore); + } else { + writeFileSync(".gitignore", baseGitignore); + } + + if (!existsSync("sda")) mkdirSync("sda", { recursive: true }); + writeFileSync("sda/main.ts", getMainTs(example, false)); + if (!existsSync("sda/helpers")) mkdirSync("sda/helpers", { recursive: true }); + if (example) writeFileSync("sda/helpers/crunchData.ts", getCrunchDataTs(false)); + if (!existsSync("sda/data")) mkdirSync("sda/data", { recursive: true }); + if (example) writeFileSync("sda/data/temp.csv", "city,decade,meanTemp\nToronto,1840.0,7.1"); + if (!existsSync("sda/output")) mkdirSync("sda/output", { recursive: true }); + + if (!existsSync("README.md")) writeFileSync("README.md", readme); + + if (args.includes("--env") && !existsSync(".env")) writeFileSync(".env", ""); + + const installCmds: any = { + nodejs: [ + "npx jsr add @nshiab/simple-data-analysis @nshiab/journalism", + "npm i @observablehq/plot", + ], + bun: [ + "bunx jsr add @nshiab/simple-data-analysis @nshiab/journalism", + "bun add @observablehq/plot", + ], + deno: [ + "deno install --node-modules-dir=auto jsr:@nshiab/simple-data-analysis jsr:@nshiab/journalism npm:@observablehq/plot", + ] + }; + + if (args.includes("--scrape")) { + installCmds.nodejs.push("npm i cheerio playwright-chromium"); + installCmds.nodejs.push("npx jsr add @std/fs"); + installCmds.bun.push("bun add cheerio playwright-chromium"); + installCmds.bun.push("bunx jsr add @std/fs"); + installCmds.deno.push("deno install --node-modules-dir=auto npm:cheerio npm:playwright-chromium jsr:@std/fs"); + } + + console.log("\n3 - Installing libraries..."); + installCmds[runtime].forEach((cmd: string) => execSync(cmd)); + + if (args.includes("--git")) { + console.log("\n4 - Initializing Git repository..."); + execSync("git init && git add -A && git commit -m 'Setup done'"); + } + + console.log("\nSetup is done!\n"); +} diff --git a/src/actions/svelte.ts b/src/actions/svelte.ts new file mode 100644 index 0000000..e015a8d --- /dev/null +++ b/src/actions/svelte.ts @@ -0,0 +1,223 @@ +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import { Buffer } from "node:buffer"; +import { styleCss, highlightThemeCss } from "../templates/css.ts"; +import { + getPageSvelte, + getLayoutSvelte, + tableSvelte, + selectSvelte, + radioSvelte, + CodeHighlightSvelte, + highlightSvelte, + chartSvelte, + pageTs, + layoutTs, + getLibIndexTs, + getTempChangeTs, + appDTs, + appHtml +} from "../templates/svelte.ts"; +import { + viteConfigTs, + tsconfigJson, + getSvelteConfigJs, + deployYml, + svelteGitignore, + getMainTs, + getCrunchDataTs +} from "../templates/configs.ts"; +import { Runtime } from "../lib/utils.ts"; + +export async function setupSvelte(args: string[], runtime: Runtime, execSync: any) { + if (!args.includes("--svelte")) return; + + const example = args.includes("--example"); + + console.log("\n2 - Creating relevant files..."); + + if (runtime === "nodejs") { + if (existsSync("package.json")) { + const current = JSON.parse(readFileSync("package.json", "utf-8")); + current.scripts = { ...current.scripts, + dev: "vite dev", + build: "vite build", + preview: "vite preview", + check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "node " + (args.includes("--env") ? "--env-file=.env " : "") + "--experimental-strip-types --no-warnings --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + }; + writeFileSync("package.json", JSON.stringify(current, null, 2)); + } else { + const pkg = { + type: "module", + scripts: { + dev: "vite dev", + build: "vite build", + preview: "vite preview", + check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "node " + (args.includes("--env") ? "--env-file=.env " : "") + "--experimental-strip-types --no-warnings --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + } + }; + writeFileSync("package.json", JSON.stringify(pkg, null, 2)); + } + if (!existsSync("tsconfig.json")) writeFileSync("tsconfig.json", tsconfigJson); + if (!existsSync("vite.config.ts")) writeFileSync("vite.config.ts", viteConfigTs); + if (!existsSync("svelte.config.js")) writeFileSync("svelte.config.js", getSvelteConfigJs(args.includes("--pages"))); + } else if (runtime === "bun") { + if (existsSync("package.json")) { + const current = JSON.parse(readFileSync("package.json", "utf-8")); + current.scripts = { ...current.scripts, + dev: "vite dev", + build: "vite build", + preview: "vite preview", + check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "bun --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + }; + writeFileSync("package.json", JSON.stringify(current, null, 2)); + } else { + const pkg = { + type: "module", + scripts: { + dev: "vite dev", + build: "vite build", + preview: "vite preview", + check: "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "bun --watch sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp" + } + }; + writeFileSync("package.json", JSON.stringify(pkg, null, 2)); + } + if (!existsSync("tsconfig.json")) writeFileSync("tsconfig.json", tsconfigJson); + if (!existsSync("vite.config.ts")) writeFileSync("vite.config.ts", viteConfigTs); + if (!existsSync("svelte.config.js")) writeFileSync("svelte.config.js", getSvelteConfigJs(args.includes("--pages"))); + } else if (runtime === "deno") { + const denoJson: any = { + tasks: { + "dev": "vite dev", + "build": "vite build", + "preview": "vite preview", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + sda: "deno run " + (args.includes("--env") ? "--env-file " : "") + "--node-modules-dir=auto -A --watch --check sda/main.ts", + clean: "rm -rf .sda-cache && rm -rf .journalism-cache && rm -rf .tmp", + }, + nodeModulesDir: "auto", + "unstable": ["sloppy-imports", "fmt-component"], + "lint": { "rules": { "exclude": ["no-sloppy-imports"] } }, + "imports": { "$lib": "./src/lib/index.ts", "$lib/": "./src/lib/" }, + "compilerOptions": { + "rootDirs": [".", "./.svelte-kit/types"], + "verbatimModuleSyntax": true, + "lib": ["esnext", "DOM", "DOM.Iterable", "deno.ns"], + "types": ["./.svelte-kit/ambient.d.ts", "./.svelte-kit/non-ambient.d.ts"], + }, + }; + if (existsSync("deno.json")) { + const current = JSON.parse(readFileSync("deno.json", "utf-8")); + current.tasks = { ...current.tasks, ...denoJson.tasks }; + writeFileSync("deno.json", JSON.stringify(current, null, 2)); + } else { + writeFileSync("deno.json", JSON.stringify(denoJson, null, 2)); + } + if (!existsSync("tsconfig.json")) writeFileSync("tsconfig.json", tsconfigJson); + if (!existsSync("vite.config.ts")) writeFileSync("vite.config.ts", viteConfigTs); + if (!existsSync("svelte.config.js")) writeFileSync("svelte.config.js", getSvelteConfigJs(args.includes("--pages"))); + } + + if (existsSync(".gitignore")) { + const current = readFileSync(".gitignore", "utf-8"); + writeFileSync(".gitignore", current + "\n\n# Added by setup-sda\n" + svelteGitignore); + } else { + writeFileSync(".gitignore", svelteGitignore); + } + + if (!existsSync(".svelte-kit")) mkdirSync(".svelte-kit", { recursive: true }); + writeFileSync(".svelte-kit/ambient.d.ts", ""); + writeFileSync(".svelte-kit/non-ambient.d.ts", ""); + + if (!existsSync("static")) mkdirSync("static", { recursive: true }); + writeFileSync("static/style.css", styleCss); + writeFileSync("static/highlight-theme.css", highlightThemeCss); + + try { + const res = await fetch("https://svelte.dev/favicon.png"); + const arrayBuffer = await res.arrayBuffer(); + writeFileSync("static/favicon.png", Buffer.from(arrayBuffer)); + } catch (e) { console.error(" => static/favicon.png could not be created."); } + + if (example) writeFileSync("static/temp.json", JSON.stringify([])); + + if (!existsSync("src")) mkdirSync("src", { recursive: true }); + writeFileSync("src/app.html", appHtml); + writeFileSync("src/app.d.ts", appDTs); + + if (!existsSync("src/routes")) mkdirSync("src/routes", { recursive: true }); + writeFileSync("src/routes/+page.svelte", getPageSvelte(example)); + if (example) writeFileSync("src/routes/+page.ts", pageTs); + writeFileSync("src/routes/+layout.ts", layoutTs); + writeFileSync("src/routes/+layout.svelte", getLayoutSvelte(example, args.includes("--pages"))); + + if (!existsSync("src/lib")) mkdirSync("src/lib", { recursive: true }); + writeFileSync("src/lib/index.ts", getLibIndexTs(example)); + + if (!existsSync("src/helpers")) mkdirSync("src/helpers", { recursive: true }); + if (example) writeFileSync("src/helpers/getTempChange.ts", getTempChangeTs); + + if (!existsSync("src/components")) mkdirSync("src/components", { recursive: true }); + writeFileSync("src/components/Table.svelte", tableSvelte); + writeFileSync("src/components/Select.svelte", selectSvelte); + writeFileSync("src/components/Radio.svelte", radioSvelte); + writeFileSync("src/components/CodeHighlight.svelte", CodeHighlightSvelte); + writeFileSync("src/components/Highlight.svelte", highlightSvelte); + if (example) writeFileSync("src/components/Chart.svelte", chartSvelte); + + if (!existsSync("src/data")) mkdirSync("src/data", { recursive: true }); + + if (!existsSync("sda")) mkdirSync("sda", { recursive: true }); + writeFileSync("sda/main.ts", getMainTs(example, true)); + if (!existsSync("sda/helpers")) mkdirSync("sda/helpers", { recursive: true }); + if (example) writeFileSync("sda/helpers/crunchData.ts", getCrunchDataTs(true)); + if (!existsSync("sda/data")) mkdirSync("sda/data", { recursive: true }); + if (example) writeFileSync("sda/data/temp.csv", "city,decade,meanTemp\nToronto,1840.0,7.1"); + if (!existsSync("sda/output")) mkdirSync("sda/output", { recursive: true }); + + if (args.includes("--pages")) { + if (!existsSync(".github")) mkdirSync(".github", { recursive: true }); + if (!existsSync(".github/workflows")) mkdirSync(".github/workflows", { recursive: true }); + writeFileSync(".github/workflows/deploy.yml", deployYml); + } + + if (args.includes("--env") && !existsSync(".env")) writeFileSync(".env", ""); + + const installCmds: any = { + nodejs: [ + "npm i @sveltejs/adapter-auto @sveltejs/adapter-static @sveltejs/kit @sveltejs/vite-plugin-svelte svelte svelte-check typescript vite highlight.js @observablehq/plot --save-dev", + "npx jsr add @nshiab/simple-data-analysis @nshiab/journalism", + ], + bun: [ + "bun add @sveltejs/adapter-auto @sveltejs/adapter-static @sveltejs/kit @sveltejs/vite-plugin-svelte svelte svelte-check typescript vite highlight.js @observablehq/plot --dev", + "bunx jsr add @nshiab/simple-data-analysis @nshiab/journalism", + ], + deno: [ + "deno install --node-modules-dir=auto --dev --allow-scripts=npm:@sveltejs/kit npm:@sveltejs/adapter-auto npm:@sveltejs/adapter-static npm:@sveltejs/kit npm:@sveltejs/vite-plugin-svelte npm:svelte npm:svelte-check npm:typescript npm:vite npm:highlight.js npm:@observablehq/plot jsr:@nshiab/simple-data-analysis jsr:@nshiab/journalism", + ] + }; + + if (args.includes("--scrape")) { + installCmds.nodejs.push("npm i cheerio playwright-chromium"); + installCmds.nodejs.push("npx jsr add @std/fs"); + installCmds.bun.push("bun add cheerio playwright-chromium"); + installCmds.bun.push("bunx jsr add @std/fs"); + installCmds.deno.push("deno install --node-modules-dir=auto npm:cheerio npm:playwright-chromium jsr:@std/fs"); + } + + console.log("\n3 - Installing libraries with " + (runtime === "nodejs" ? "NPM" : runtime === "bun" ? "Bun" : "Deno") + "..."); + installCmds[runtime].forEach((cmd: string) => execSync(cmd)); +} diff --git a/src/lib/utils.ts b/src/lib/utils.ts new file mode 100644 index 0000000..ce5187e --- /dev/null +++ b/src/lib/utils.ts @@ -0,0 +1,27 @@ +import { execSync as nodeExecSync } from "node:child_process"; +import process from "node:process"; + +export type Runtime = "bun" | "deno" | "nodejs"; + +export function getRuntime(): Runtime { + const userAgent = (globalThis.navigator?.userAgent || "node").toLocaleLowerCase(); + if (userAgent.includes("bun")) return "bun"; + if (userAgent.includes("deno")) return "deno"; + return "nodejs"; +} + +export function createExecSync(isTest: boolean) { + return (command: string, options: any = { stdio: "ignore" }) => { + if (isTest && ( + command.includes("npm ") || + command.includes("npx ") || + command.includes("bun ") || + command.includes("bunx ") || + command.includes("deno install") + )) { + console.log(` => Skipping: ${command}`); + } else { + nodeExecSync(command, options); + } + }; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..54d1462 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,47 @@ +import process from "node:process"; +import { getRuntime, createExecSync } from "./lib/utils.ts"; +import { setupAgents } from "./actions/agents.ts"; +import { setupSvelte } from "./actions/svelte.ts"; +import { setupBase } from "./actions/base.ts"; + +async function main() { + const args = process.argv.slice(2); + const isTest = args.includes("--test"); + const runtime = getRuntime(); + const execSync = createExecSync(isTest); + + console.log("\nStarting sda setup..."); + console.log("\n1 - Checking runtime and options..."); + + console.log(` => Your navigator userAgent is: ${navigator.userAgent}`); + + const flags = ["--test", "--git", "--scrape", "--pages", "--env", "--claude", "--gemini", "--copilot", "--agent", "--svelte", "--example"]; + flags.forEach(flag => { + if (args.includes(flag)) { + console.log(` => You passed the option ${flag}`); + } + }); + + if (args.includes("--pages") && !args.includes("--svelte")) { + console.log(` => You passed the option --pages, but it only works with --svelte`); + } + + // Phase 1: Agents & Documentation + await setupAgents(args); + + // Phase 2: Project structure & Libraries + if (args.includes("--svelte")) { + await setupSvelte(args, runtime, execSync); + } else { + await setupBase(args, runtime, execSync); + } + + if (args.includes("--pages") && args.includes("--svelte")) { + console.log("PS: Don't forget to enable GitHub Pages in your repository settings.\n"); + } +} + +main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/src/templates/configs.ts b/src/templates/configs.ts new file mode 100644 index 0000000..ad9f671 --- /dev/null +++ b/src/templates/configs.ts @@ -0,0 +1,269 @@ +export const viteConfigTs = `import { sveltekit } from "@sveltejs/kit/vite"; +import { defineConfig } from "vite"; + +export default defineConfig({ + plugins: [sveltekit()], +}); +`; + +export const tsconfigJson = `{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + } + // Path aliases are handled by https://svelte.dev/docs/kit/configuration#alias + // except $lib which is handled by https://svelte.dev/docs/kit/configuration#files + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} +`; + +export function getSvelteConfigJs(pages: boolean) { + if (pages) { + return `import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; +import process from "node:process"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + paths: { + base: process.argv.includes("dev") ? "" : process.env.BASE_PATH, + }, + }, +}; + +export default config;`; + } else { + return `import adapter from "@sveltejs/adapter-static"; +import { vitePreprocess } from "@sveltejs/vite-plugin-svelte"; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + preprocess: vitePreprocess(), + kit: { + adapter: adapter(), + }, +}; + +export default config;`; + } +} + +export const deployYml = `name: Deploy to GitHub Pages + +on: + push: + branches: "main" + +jobs: + build_site: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Deno + uses: denoland/setup-deno@v2 + with: + deno-version: v2.x + + - name: Install dependencies + run: deno install + + - name: build + env: + BASE_PATH: "/\${{ github.event.repository.name }}" + run: deno task build + + - name: Upload Artifacts + uses: actions/upload-pages-artifact@v3 + with: + path: "build/" + + deploy: + needs: build_site + runs-on: ubuntu-latest + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + url: \${{ steps.deployment.outputs.page_url }} + + steps: + - name: Deploy + id: deployment + uses: actions/deploy-pages@v4 +`; + +export const svelteGitignore = `node_modules + +# Output +.output +.vercel +/.svelte-kit +/build + +# OS +.DS_Store +Thumbs.db + +# Env +.env +.env.* +!.env.example +!.env.test + +# Vite +vite.config.js.timestamp-* +vite.config.ts.timestamp-* + +# Added by setup-sda +.tmp +.sda-cache +.journalism-cache +sda/data`; + +export const baseGitignore = `# Added by setup-sda +node_modules +.tmp +.sda-cache +.journalism-cache +sda/data +.env +.DS_Store`; + +export function getMainTs(example: boolean, svelte: boolean) { + if (svelte) { + if (example) { + return `import { SimpleDB } from "@nshiab/simple-data-analysis"; +import crunchData from "./helpers/crunchData.ts"; + +const sdb = new SimpleDB({ logDuration: true }); + +await crunchData(sdb); + +await sdb.done();`; + } else { + return `import { SimpleDB } from "@nshiab/simple-data-analysis"; + +const sdb = new SimpleDB(); + +// Do your magic here... + +await sdb.done();`; + } + } else { + // Non-svelte example + if (example) { + return `import { SimpleDB } from "@nshiab/simple-data-analysis"; +import crunchData from "./helpers/crunchData.ts"; + +const sdb = new SimpleDB({ logDuration: true}); + +await crunchData(sdb); + +await sdb.done(); + +`; + } else { + return `import { SimpleDB } from "@nshiab/simple-data-analysis"; + +const sdb = new SimpleDB(); + +// Do your magic here! + +await sdb.done(); +`; + } + } +} + +export function getCrunchDataTs(svelte: boolean) { + if (svelte) { + return `import type { SimpleDB } from "@nshiab/simple-data-analysis"; + +export default async function crunchData(sdb: SimpleDB) { + + // The mean temperature per decade. + const temp = sdb.newTable("temp"); + await temp.loadData("sda/data/temp.csv"); + await temp.logTable(); + + // We compute a linear regression for each city. + const tempRegressions = await temp.linearRegressions({ + x: "decade", + y: "meanTemp", + categories: "city", + decimals: 4, + outputTable: "tempRegressions", + }); + await tempRegressions.logTable(); + + // We write the results to src/data + await tempRegressions.writeData("src/data/temp-regressions.json"); + // Or to static + await temp.writeData("static/temp.json"); +}`; + } else { + return `import type { SimpleDB } from "@nshiab/simple-data-analysis"; + +export default async function crunchData(sdb: SimpleDB) { + + // The mean temperature per decade. + const temp = sdb.newTable("temp"); + await temp.loadData("sda/data/temp.csv"); + + // We log the table. + await temp.logTable(); + + // We log line charts. + await temp.logLineChart("decade", "meanTemp", { + smallMultiples: "city", + fixedScales: true, + formatY: (d) => \\\`\\\${d}°C\\\`, + }); + + // We compute a linear regression for each city. + const tempRegressions = await temp.linearRegressions({ + x: "decade", + y: "meanTemp", + categories: "city", + decimals: 4, + outputTable: "tempRegressions", + }); + await tempRegressions.logTable(); + + // We write the results to src/output. + await tempRegressions.writeData("sda/output/temp-regressions.json"); +} +`; + } +} + +export const baseTsconfigJson = `{ + "compilerOptions": { + "module": "NodeNext", + "allowImportingTsExtensions": true, + "noEmit": true, + "verbatimModuleSyntax": null + }, + "include": ["**/*"], + "exclude": ["node_modules"] +} +`; diff --git a/src/templates/css.ts b/src/templates/css.ts new file mode 100644 index 0000000..25b0d4d --- /dev/null +++ b/src/templates/css.ts @@ -0,0 +1,853 @@ +export const styleCss = `/* Adapted from https://github.com/kevquirk/simple.css */ + +/* Global variables. */ +:root { + /* Set sans-serif & mono fonts */ + --sans-font: + -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir, "Nimbus Sans L", + Roboto, "Noto Sans", "Segoe UI", Arial, Helvetica, "Helvetica Neue", + sans-serif; + --mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace; + --standard-border-radius: 5px; + + /* Default (light) theme */ + --bg: #fff; + --accent-bg: #f5f5f5; + /* --accent-bg: #f5f7ff; */ + --text: #212121; + --text-light: #585858; + --border: #898ea4; + --accent: #0d47a1; + --accent-hover: #1266e2; + --accent-text: var(--bg); + --code: #d81b60; + --preformatted: #444; + --marked: #ffdd33; + --disabled: #efefef; +} + +/* Dark theme */ +/* @media (prefers-color-scheme: dark) { + :root { + color-scheme: dark; + --bg: #212121; + --accent-bg: #2b2b2b; + --text: #dcdcdc; + --text-light: #ababab; + --accent: #ffb300; + --accent-hover: #ffe099; + --accent-text: var(--bg); + --code: #f06292; + --preformatted: #ccc; + --disabled: #111; + } + img, + video { + opacity: 0.8; + } +} */ + +/* Reset box-sizing */ +*, *::before, *::after { + box-sizing: border-box; +} + +/* Reset default appearance */ +textarea, +select, +input, +progress { + appearance: none; + -webkit-appearance: none; + -moz-appearance: none; +} + +html { + /* Set the font globally */ + font-family: var(--sans-font); + scroll-behavior: smooth; +} + +/* Make the body a nice central block */ +body { + color: var(--text); + background-color: var(--bg); + font-size: 1.15rem; + line-height: 1.5; + display: grid; + grid-template-columns: 1fr min(45rem, 90%) 1fr; + margin: 0; +} +body > div > * { + grid-column: 2; +} + +/* Make the header bg full width, but the content inline with body */ +body > div > header { + background-color: var(--accent-bg); + border-bottom: 1px solid var(--border); + text-align: center; + padding: 0 0.5rem 2rem 0.5rem; + grid-column: 1 / -1; +} + +body > div > header > *:only-child { + margin-block-start: 2rem; +} + +body > div > header h1 { + max-width: 1200px; + margin: 1rem auto; +} + +body > div > header p { + max-width: 40rem; + margin: 1rem auto; +} + +/* Add a little padding to ensure spacing is correct between content and header > nav */ +main { + padding-top: 1.5rem; +} + +body > div > footer { + margin-top: 4rem; + padding: 2rem 1rem 1.5rem 1rem; + color: var(--text-light); + font-size: 0.9rem; + text-align: center; + border-top: 1px solid var(--border); +} + +/* Format headers */ +h1 { + font-size: 3rem; +} + +h2 { + font-size: 2.6rem; + margin-top: 3rem; +} + +h3 { + font-size: 2rem; + margin-top: 3rem; +} + +h4 { + font-size: 1.44rem; +} + +h5 { + font-size: 1.15rem; +} + +h6 { + font-size: 0.96rem; +} + +p { + margin: 1.5rem 0; +} + +/* Prevent long strings from overflowing container */ +p, h1, h2, h3, h4, h5, h6 { + overflow-wrap: break-word; +} + +/* Fix line height when title wraps */ +h1, +h2, +h3 { + line-height: 1.1; +} + +/* Reduce header size on mobile */ +@media only screen and (max-width: 720px) { + h1 { + font-size: 2.5rem; + } + + h2 { + font-size: 2.1rem; + } + + h3 { + font-size: 1.75rem; + } + + h4 { + font-size: 1.25rem; + } +} + +/* Format links & buttons */ +a, +a:visited { + color: var(--accent); +} + +a:hover { + text-decoration: none; +} + +button, +.button, +a.button, /* extra specificity to override a */ +input[type="submit"], +input[type="reset"], +input[type="button"] { + border: 1px solid var(--accent); + background-color: var(--accent); + color: var(--accent-text); + padding: 0.5rem 0.9rem; + text-decoration: none; + line-height: normal; +} + +.button[aria-disabled="true"], +input:disabled, +textarea:disabled, +select:disabled, +button[disabled] { + cursor: not-allowed; + background-color: var(--disabled); + border-color: var(--disabled); + color: var(--text-light); +} + +input[type="range"] { + padding: 0; +} + +/* Set the cursor to '?' on an abbreviation and style the abbreviation to show that there is more information underneath */ +abbr[title] { + cursor: help; + text-decoration-line: underline; + text-decoration-style: dotted; +} + +button:enabled:hover, +.button:not([aria-disabled="true"]):hover, +input[type="submit"]:enabled:hover, +input[type="reset"]:enabled:hover, +input[type="button"]:enabled:hover { + background-color: var(--accent-hover); + border-color: var(--accent-hover); + cursor: pointer; +} + +.button:focus-visible, +button:focus-visible:where(:enabled), +input:enabled:focus-visible:where( + [type="submit"], + [type="reset"], + [type="button"] +) { + outline: 2px solid var(--accent); + outline-offset: 1px; +} + +/* Format navigation */ +header > nav { + font-size: 1rem; + line-height: 2; + padding: 1rem 0 0 0; +} + +/* Use flexbox to allow items to wrap, as needed */ +header > nav ul, +header > nav ol { + align-content: space-around; + align-items: center; + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + list-style-type: none; + margin: 0; + padding: 0; +} + +/* List items are inline elements, make them behave more like blocks */ +header > nav ul li, +header > nav ol li { + display: inline-block; +} + +header > nav a, +header > nav a:visited { + margin: 0 0.5rem 1rem 0.5rem; + border: 1px solid var(--border); + border-radius: var(--standard-border-radius); + color: var(--text); + display: inline-block; + padding: 0.1rem 1rem; + text-decoration: none; +} + +header > nav a:hover, +header > nav a.current, +header > nav a[aria-current="page"], +header > nav a[aria-current="true"] { + border-color: var(--accent); + color: var(--accent); + cursor: pointer; +} + +/* Reduce nav side on mobile */ +@media only screen and (max-width: 720px) { + header > nav a { + border: none; + padding: 0; + text-decoration: underline; + line-height: 1; + } +} + +/* Consolidate box styling */ +aside, +details, +/* pre, */ +progress { + background-color: var(--accent-bg); + border: 1px solid var(--border); + border-radius: var(--standard-border-radius); + margin-bottom: 1rem; +} + +pre { + border-radius: var(--standard-border-radius); + background-color: #f9f9f9 !important; +} + +aside { + font-size: 1rem; + width: 30%; + padding: 0 15px; + margin-inline-start: 15px; + float: right; +} +*[dir="rtl"] aside { + float: left; +} + +/* Make aside full-width on mobile */ +@media only screen and (max-width: 720px) { + aside { + width: 100%; + float: none; + margin-inline-start: 0; + } +} + +article, fieldset, dialog { + border: 1px solid var(--border); + padding: 1rem; + border-radius: var(--standard-border-radius); + margin-bottom: 1rem; +} + +article h2:first-child, +section h2:first-child, +article h3:first-child, +section h3:first-child { + margin-top: 1rem; +} + +section { + border-top: 1px solid var(--border); + border-bottom: 1px solid var(--border); + padding: 2rem 1rem; + margin: 3rem 0; +} + +/* Don't double separators when chaining sections */ +section + section, +section:first-child { + border-top: 0; + padding-top: 0; +} + +section + section { + margin-top: 0; +} + +section:last-child { + border-bottom: 0; + padding-bottom: 0; +} + +details { + padding: 0.7rem 1rem; +} + +summary { + cursor: pointer; + font-weight: bold; + padding: 0.7rem 1rem; + margin: -0.7rem -1rem; + word-break: break-all; +} + +details[open] > summary + * { + margin-top: 0; +} + +details[open] > summary { + margin-bottom: 0.5rem; +} + +details[open] > :last-child { + margin-bottom: 0; +} + +/* Format tables */ +table { + border-collapse: collapse; + margin: 1.5rem 0; +} + +figure > table { + width: max-content; + margin: 0; +} + +td, +th { + border: 1px solid var(--border); + text-align: start; + padding: 0.5rem; +} + +th { + background-color: var(--accent-bg); + font-weight: bold; +} + +tr:nth-child(even) { + /* Set every other cell slightly darker. Improves readability. */ + background-color: var(--accent-bg); +} + +table caption { + font-weight: bold; + margin-bottom: 0.5rem; +} + +/* Format forms */ +textarea, +select, +input, +button, +.button { + font-size: inherit; + font-family: inherit; + padding: 0.5rem; + margin-bottom: 0.5rem; + border-radius: var(--standard-border-radius); + box-shadow: none; + max-width: 100%; + display: inline-block; +} +textarea, +select, +input { + color: var(--text); + background-color: var(--bg); + border: 1px solid var(--border); +} +label { + /* display: block; */ + margin-right: 4px; +} +textarea:not([cols]) { + width: 100%; +} + +/* Add arrow to drop-down */ +select:not([multiple]) { + background-image: + linear-gradient(45deg, transparent 49%, var(--text) 51%), + linear-gradient(135deg, var(--text) 51%, transparent 49%); + background-position: + calc(100% - 15px), + calc(100% - 10px); + background-size: 5px 5px, 5px 5px; + background-repeat: no-repeat; + padding-inline-end: 25px; +} +*[dir="rtl"] select:not([multiple]) { + background-position: 10px, 15px; +} + +/* checkbox and radio button style */ +input[type="checkbox"], +input[type="radio"] { + vertical-align: middle; + position: relative; + width: min-content; +} + +input[type="checkbox"] + label, +input[type="radio"] + label { + display: inline-block; +} + +input[type="radio"] { + border-radius: 100%; +} + +input[type="checkbox"]:checked, +input[type="radio"]:checked { + background-color: var(--accent); +} + +input[type="checkbox"]:checked::after { + /* Creates a rectangle with colored right and bottom borders which is rotated to look like a check mark */ + content: " "; + width: 0.18em; + height: 0.32em; + border-radius: 0; + position: absolute; + top: 0.05em; + left: 0.17em; + background-color: transparent; + border-right: solid var(--bg) 0.08em; + border-bottom: solid var(--bg) 0.08em; + font-size: 1.8em; + transform: rotate(45deg); +} +input[type="radio"]:checked::after { + /* creates a colored circle for the checked radio button */ + content: " "; + width: 0.25em; + height: 0.25em; + border-radius: 100%; + position: absolute; + top: 0.125em; + background-color: var(--bg); + left: 0.125em; + font-size: 32px; +} + +/* Makes input fields wider on smaller screens */ +@media only screen and (max-width: 720px) { + textarea, + select, + input { + width: 100%; + } +} + +/* Set a height for color input */ +input[type="color"] { + height: 2.5rem; + padding: 0.2rem; +} + +/* do not show border around file selector button */ +input[type="file"] { + border: 0; +} + +/* Misc body elements */ +hr { + border: none; + height: 1px; + background: var(--border); + margin: 1rem auto; +} + +mark { + padding: 2px 5px; + border-radius: var(--standard-border-radius); + background-color: var(--marked); + color: black; +} + +mark a { + color: #0d47a1; +} + +img, +video { + max-width: 100%; + height: auto; + border-radius: var(--standard-border-radius); +} + +figure { + margin: 0; + display: block; + overflow-x: auto; +} + +figure > img, +figure > picture > img { + display: block; + margin-inline: auto; +} + +figcaption { + /* text-align: center; */ + font-size: 0.9rem; + color: var(--text-light); + margin-top: 0.5rem; + margin-bottom: 1rem; +} + +blockquote { + margin-inline-start: 2rem; + margin-inline-end: 0; + margin-block: 2rem; + padding: 0.4rem 0.8rem; + border-inline-start: 0.35rem solid var(--accent); + color: var(--text-light); + font-style: italic; +} + +cite { + font-size: 0.9rem; + color: var(--text-light); + font-style: normal; +} + +dt { + color: var(--text-light); +} + +/* Use mono font for code elements */ +code, +pre, +pre span, +kbd, +samp { + font-family: var(--mono-font); + color: var(--code); +} + +kbd { + color: var(--preformatted); + border: 1px solid var(--preformatted); + border-bottom: 3px solid var(--preformatted); + border-radius: var(--standard-border-radius); + padding: 0.1rem 0.4rem; +} + +pre { + padding: 1rem 1.4rem; + max-width: 100%; + overflow: auto; + color: var(--preformatted); +} + +/* Fix embedded code within pre */ +pre code { + color: var(--preformatted); + background: none; + margin: 0; + padding: 0; +} + +/* Progress bars */ +/* Declarations are repeated because you */ +/* cannot combine vendor-specific selectors */ +progress { + width: 100%; +} + +progress:indeterminate { + background-color: var(--accent-bg); +} + +progress::-webkit-progress-bar { + border-radius: var(--standard-border-radius); + background-color: var(--accent-bg); +} + +progress::-webkit-progress-value { + border-radius: var(--standard-border-radius); + background-color: var(--accent); +} + +progress::-moz-progress-bar { + border-radius: var(--standard-border-radius); + background-color: var(--accent); + transition-property: width; + transition-duration: 0.3s; +} + +progress:indeterminate::-moz-progress-bar { + background-color: var(--accent-bg); +} + +dialog { + background-color: var(--bg); + max-width: 40rem; + margin: auto; +} + +dialog::backdrop { + background-color: var(--bg); + opacity: 0.8; +} + +@media only screen and (max-width: 720px) { + dialog { + max-width: 100%; + margin: auto 1em; + } +} + +/* Superscript & Subscript */ +/* Prevent scripts from affecting line-height. */ +sup, sub { + vertical-align: baseline; + position: relative; +} + +sup { + top: -0.4em; +} + +sub { + top: 0.3em; +} + +/* Classes for notices */ +.notice { + background: var(--accent-bg); + border: 2px solid var(--border); + border-radius: var(--standard-border-radius); + padding: 1.5rem; + margin: 2rem 0; +} +`; + +export const highlightThemeCss = `/* + Theme: GitHub + Description: Light theme as seen on github.com + Author: github.com + Maintainer: @Hirse + Updated: 2021-05-15 + + Outdated base version: https://github.com/primer/github-syntax-light + Current colors taken from GitHub's CSS +*/ + +.hljs { + color: #24292e; + background: #ffffff; +} + +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + /* prettylights-syntax-keyword */ + color: #d73a49; +} + +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + /* prettylights-syntax-entity */ + color: #6f42c1; +} + +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, +.hljs-number, +.hljs-operator, +.hljs-variable, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id { + /* prettylights-syntax-constant */ + color: #005cc5; +} + +.hljs-regexp, +.hljs-string, +.hljs-meta .hljs-string { + /* prettylights-syntax-string */ + color: #032f62; +} + +.hljs-built_in, +.hljs-symbol { + /* prettylights-syntax-variable */ + color: #e36209; +} + +.hljs-comment, +.hljs-code, +.hljs-formula { + /* prettylights-syntax-comment */ + color: #6a737d; +} + +.hljs-name, +.hljs-quote, +.hljs-selector-tag, +.hljs-selector-pseudo { + /* prettylights-syntax-entity-tag */ + color: #22863a; +} + +.hljs-subst { + /* prettylights-syntax-storage-modifier-import */ + color: #24292e; +} + +.hljs-section { + /* prettylights-syntax-markup-heading */ + color: #005cc5; + font-weight: bold; +} + +.hljs-bullet { + /* prettylights-syntax-markup-list */ + color: #735c0f; +} + +.hljs-emphasis { + /* prettylights-syntax-markup-italic */ + color: #24292e; + font-style: italic; +} + +.hljs-strong { + /* prettylights-syntax-markup-bold */ + color: #24292e; + font-weight: bold; +} + +.hljs-addition { + /* prettylights-syntax-markup-inserted */ + color: #22863a; + background-color: #f0fff4; +} + +.hljs-deletion { + /* prettylights-syntax-markup-deleted */ + color: #b31d28; + background-color: #ffeef0; +} +/* +.hljs-char.escape_, +.hljs-link, +.hljs-params, +.hljs-property, +.hljs-punctuation, +.hljs-tag { + +} */ +`; diff --git a/src/templates/svelte.ts b/src/templates/svelte.ts new file mode 100644 index 0000000..36771bb --- /dev/null +++ b/src/templates/svelte.ts @@ -0,0 +1,529 @@ +export function getPageSvelte(example: boolean) { + if (example) { + return ` + +

Climate change

+ + +
+
+ + +
+ +{/if} + +
= 5 + ? "margin-top: 0.75rem; margin-bottom: 0.50rem" + : ""} +> + + + + {#each columns as column} + + {/each} + + + + {#each rows as row} + + {#each row as cell} + + {/each} + + {/each} + +
{column}
{cell}
+
+ +{#if data.length > 5} +

+ Displaying {Math.max(0, endRow + 1 - startRow)} items out of {data.length}. +

+{/if}`; + +export const selectSvelte = ` + + + +`; + +export const radioSvelte = ` + +
+

{label}

+ {#each values as val, i} + + {/each} +
+`; + +export const CodeHighlightSvelte = ` + +
+

+ {filename} + +

+
+
+`; + +export const highlightSvelte = ` + + + {text} +`; + +export const chartSvelte = ` + +{#key [city, width]} +
+{/key}`; + +export const pageTs = `// Types automatically generated by SvelteKit. +import type { PageLoad } from "./$types"; +// Custom types. +import type { tempT } from "$lib"; + +export const load: PageLoad = async ({ fetch }) => { + const res = await fetch("/temp.json"); + const temp = await res.json() as tempT; + + return { temp }; +}; +`; + +export const layoutTs = `export const prerender = true; +export const trailingSlash = "always";`; + +export function getLibIndexTs(example: boolean) { + if (example) { + return `// place files you want to import through the \$lib alias in this folder. + +type cityT = string; + +type tempT = { city: string; decade: number; meanTemp: number }[]; + +type tempRegrT = { + city: string; + slope: number; + yIntercept: number; + r2: number; +}[]; + +export type { cityT, tempRegrT, tempT };`; + } else { + return `// place files you want to import through the \$lib alias in this folder.`; + } +} + +export const getTempChangeTs = `// It's important to use the 'web' entrypoint since this is running in the browser. +import { formatNumber } from "@nshiab/journalism/web"; +import type { tempRegrT } from "../lib/index.ts"; + +export default function getTempChange( + city: string, + tempRegr: tempRegrT, +): string { + const cityRegression = tempRegr.find((r) => r.city === city); + + if (cityRegression === undefined) { + throw new Error("City " + city + " not found in tempRegr."); + } + + const slopPerDecade = cityRegression.slope * 10; + + return formatNumber(slopPerDecade, { decimals: 3, suffix: "°C" }); +} +`; + +export const appDTs = `// See https://svelte.dev/docs/kit/types#app.d.ts +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; +`; + +export const appHtml = ` + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + +`; diff --git a/tests/agents_test.ts b/tests/agents_test.ts new file mode 100644 index 0000000..34989e8 --- /dev/null +++ b/tests/agents_test.ts @@ -0,0 +1,67 @@ +import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { cleanupTempDir, createTempDir, runCLI } from "./helpers.ts"; + +const runtimes = ["node", "deno", "bun"] as const; + +for (const runtime of runtimes) { + Deno.test(`Flag --claude: ${runtime}`, async () => { + const tmpDir = createTempDir(`claude_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--claude"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, "CLAUDE.md")), true); + assertEquals(existsSync(join(tmpDir, "docs", "journalism.md")), true); + assertEquals(existsSync(join(tmpDir, "docs", "simple-data-analysis.md")), true); + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Flag --gemini: ${runtime}`, async () => { + const tmpDir = createTempDir(`gemini_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--gemini"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, "GEMINI.md")), true); + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Flag --copilot: ${runtime}`, async () => { + const tmpDir = createTempDir(`copilot_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--copilot"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, ".github", "copilot-instructions.md")), true); + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Flag --agent: ${runtime}`, async () => { + const tmpDir = createTempDir(`agent_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--agent"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, "AGENTS.md")), true); + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Combined agents: ${runtime}`, async () => { + const tmpDir = createTempDir(`combined_agents_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--claude", "--gemini", "--copilot"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, "CLAUDE.md")), true); + assertEquals(existsSync(join(tmpDir, "GEMINI.md")), true); + assertEquals(existsSync(join(tmpDir, ".github", "copilot-instructions.md")), true); + } finally { + cleanupTempDir(tmpDir); + } + }); +} diff --git a/tests/base_test.ts b/tests/base_test.ts new file mode 100644 index 0000000..89d93f4 --- /dev/null +++ b/tests/base_test.ts @@ -0,0 +1,72 @@ +import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { cleanupTempDir, createTempDir, runCLI } from "./helpers.ts"; + +const runtimes = ["node", "deno", "bun"] as const; + +for (const runtime of runtimes) { + Deno.test(`Base setup: ${runtime}`, () => { + const tmpDir = createTempDir(`base_${runtime}`); + try { + const result = runCLI(runtime, ["--test"], tmpDir); + assertEquals(result.exitCode, 0); + + // Verify basic files are created + assertEquals(existsSync(join(tmpDir, "sda")), true); + assertEquals(existsSync(join(tmpDir, "sda", "main.ts")), true); + assertEquals(existsSync(join(tmpDir, "sda", "data")), true); + assertEquals(existsSync(join(tmpDir, "sda", "output")), true); + assertEquals(existsSync(join(tmpDir, "sda", "helpers")), true); + assertEquals(existsSync(join(tmpDir, "README.md")), true); + + const readme = readFileSync(join(tmpDir, "README.md"), "utf8"); + assertStringIncludes(readme, "setup-sda"); + + if (runtime === "deno") { + assertEquals(existsSync(join(tmpDir, "deno.json")), true); + } else { + assertEquals(existsSync(join(tmpDir, "package.json")), true); + } + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Flag --git: ${runtime}`, () => { + const tmpDir = createTempDir(`git_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--git"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, ".gitignore")), true); + const gitignore = readFileSync(join(tmpDir, ".gitignore"), "utf8"); + assertStringIncludes(gitignore, "sda/data"); + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Flag --env: ${runtime}`, () => { + const tmpDir = createTempDir(`env_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--env"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, ".env")), true); + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Flag --scrape: ${runtime}`, () => { + const tmpDir = createTempDir(`scrape_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--scrape"], tmpDir); + assertEquals(result.exitCode, 0); + // Verify skip log for scraping libraries + assertStringIncludes(result.stdout, "Skipping: "); + assertStringIncludes(result.stdout, "cheerio"); + } finally { + cleanupTempDir(tmpDir); + } + }); +} diff --git a/tests/combinations_test.ts b/tests/combinations_test.ts new file mode 100644 index 0000000..b18e24f --- /dev/null +++ b/tests/combinations_test.ts @@ -0,0 +1,33 @@ +import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { cleanupTempDir, createTempDir, runCLI } from "./helpers.ts"; + +const runtimes = ["node", "deno", "bun"] as const; + +for (const runtime of runtimes) { + Deno.test(`Kitchen Sink: ${runtime}`, async () => { + const tmpDir = createTempDir(`kitchen_sink_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--scrape", "--svelte", "--example", "--pages", "--git", "--env", "--claude"], tmpDir); + assertEquals(result.exitCode, 0); + + // Check various parts + assertEquals(existsSync(join(tmpDir, "sda", "main.ts")), true); + assertEquals(existsSync(join(tmpDir, "src", "routes", "+page.svelte")), true); + assertEquals(existsSync(join(tmpDir, ".env")), true); + assertEquals(existsSync(join(tmpDir, ".gitignore")), true); + assertEquals(existsSync(join(tmpDir, "CLAUDE.md")), true); + assertEquals(existsSync(join(tmpDir, "sda", "data", "temp.csv")), true); + + // Verify skip log for both svelte and scrape + assertStringIncludes(result.stdout, "Skipping: "); + if (runtime === "node") { + assertStringIncludes(result.stdout, "@sveltejs/kit"); + assertStringIncludes(result.stdout, "cheerio"); + } + } finally { + cleanupTempDir(tmpDir); + } + }); +} diff --git a/tests/helpers.ts b/tests/helpers.ts new file mode 100644 index 0000000..529f3fb --- /dev/null +++ b/tests/helpers.ts @@ -0,0 +1,48 @@ +import { execSync } from "node:child_process"; +import { mkdirSync, rmSync } from "node:fs"; +import { join } from "node:path"; + +export interface RunResult { + stdout: string; + stderr: string; + exitCode: number; +} + +export function runCLI( + runtime: "node" | "deno" | "bun", + args: string[], + cwd: string, +): RunResult { + const scriptPath = join(Deno.cwd(), "setup-sda.mjs"); + let command = ""; + + if (runtime === "node") { + command = `node ${scriptPath} ${args.join(" ")}`; + } else if (runtime === "deno") { + command = `deno run -A ${scriptPath} ${args.join(" ")}`; + } else if (runtime === "bun") { + command = `bun run ${scriptPath} ${args.join(" ")}`; + } + + try { + const stdout = execSync(command, { cwd, encoding: "utf8" }); + return { stdout, stderr: "", exitCode: 0 }; + } catch (error: any) { + return { + stdout: error.stdout || "", + stderr: error.stderr || "", + exitCode: error.status || 1, + }; + } +} + +export function createTempDir(name: string): string { + const tmpDir = join(Deno.cwd(), "tests", "tmp", name); + rmSync(tmpDir, { recursive: true, force: true }); + mkdirSync(tmpDir, { recursive: true }); + return tmpDir; +} + +export function cleanupTempDir(dir: string) { + rmSync(dir, { recursive: true, force: true }); +} diff --git a/tests/svelte_test.ts b/tests/svelte_test.ts new file mode 100644 index 0000000..ceaebf1 --- /dev/null +++ b/tests/svelte_test.ts @@ -0,0 +1,46 @@ +import { assertEquals, assertStringIncludes } from "https://deno.land/std@0.224.0/assert/mod.ts"; +import { existsSync, readFileSync } from "node:fs"; +import { join } from "node:path"; +import { cleanupTempDir, createTempDir, runCLI } from "./helpers.ts"; + +const runtimes = ["node", "deno", "bun"] as const; + +for (const runtime of runtimes) { + Deno.test(`Flag --svelte: ${runtime}`, async () => { + const tmpDir = createTempDir(`svelte_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--svelte"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, "src", "routes", "+page.svelte")), true); + assertEquals(existsSync(join(tmpDir, "vite.config.ts")), true); + assertEquals(existsSync(join(tmpDir, "tsconfig.json")), true); + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Flag --svelte --example: ${runtime}`, async () => { + const tmpDir = createTempDir(`svelte_example_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--svelte", "--example"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, "sda", "helpers", "crunchData.ts")), true); + assertEquals(existsSync(join(tmpDir, "sda", "data", "temp.csv")), true); + } finally { + cleanupTempDir(tmpDir); + } + }); + + Deno.test(`Flag --svelte --pages: ${runtime}`, async () => { + const tmpDir = createTempDir(`svelte_pages_${runtime}`); + try { + const result = runCLI(runtime, ["--test", "--svelte", "--pages"], tmpDir); + assertEquals(result.exitCode, 0); + assertEquals(existsSync(join(tmpDir, "src", "routes", "+layout.ts")), true); + const layoutTs = readFileSync(join(tmpDir, "src", "routes", "+layout.ts"), "utf8"); + assertStringIncludes(layoutTs, "export const prerender = true;"); + } finally { + cleanupTempDir(tmpDir); + } + }); +} From c03dddc74948713b4bafda2ebd59ddf06d61f1c7 Mon Sep 17 00:00:00 2001 From: nshiab Date: Tue, 3 Mar 2026 10:50:41 -0500 Subject: [PATCH 2/2] CI: configure git user for tests --- .github/workflows/tests.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 758d6cf..cfd8e54 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -28,6 +28,11 @@ jobs: with: bun-version: latest + - name: Configure Git + run: | + git config --global user.email "you@example.com" + git config --global user.name "Your Name" + - name: Build run: deno task build