foobara-empty-typescript-react-project-generator 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5766d1ac0b265272cc62b2da7fa015890bceb2cce99f9def87d71e7b2eb102a4
4
- data.tar.gz: 773f8ead5b569f765f9018e0c69b9131cdc55d6cc606f2cdbf8c951508952fc4
3
+ metadata.gz: 7f7b4ac21e56483eddd6d15620cf1edaaccd9448e227d17dd2fc73f33c1d38d6
4
+ data.tar.gz: 7b6a39dbbf4e8e24e739af7cc782677f5dc10139d4971d40b473935c5763a030
5
5
  SHA512:
6
- metadata.gz: c6bb08aa04721d807cfc8af0d19d03d88000d752e62e190becb20737dfd416cc05a31a2faf12305117c0c5766fd8e7e82fb70b7b320ba74a9754a948a53e8505
7
- data.tar.gz: b7ea9b1d7f2a04e4e18b8c866ab1148cdeed2fb07a920332fb2a6608d985be7e926c6b5c1acff3211e1ccc9ce44edd8a44767e932ba60ffdb9310317808ad2b2
6
+ metadata.gz: 324c237a91f5351a2b885d541b364d6593c5341960cdcf021851926f841ffdb21815bee05dc1b46926808eed81c400a21d5e397e8ae919462e8af9d889306151
7
+ data.tar.gz: cae37cc52abad3774ec99bfbc02c2719cee244d900f6a2efe9384177011302837cf27a6a6d5bcae07964d3ff7145b73c2cd0d008745f31bb8ab262d0e4488821
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## [0.3.0] - 2026-07-05
2
+
3
+ - Wire up react router/nav
4
+ - Wire up a sitemap/prerender stuff
5
+ - Switch to oxlint in generated projects
6
+
1
7
  ## [0.2.0] - 2026-03-19
2
8
 
3
9
  - Switch to generating the project with Vite instead of CRA
@@ -8,6 +8,7 @@ module Foobara
8
8
  project_dir :string, :required
9
9
  github_organization :string
10
10
  push_to_github :boolean, default: false
11
+ site_url :string, default: "https://example.com"
11
12
  end
12
13
  end
13
14
  end
@@ -7,8 +7,10 @@ module Foobara
7
7
  def manifest_to_generator_classes(manifest)
8
8
  case manifest
9
9
  when EmptyTypescriptReactProjectConfig
10
- # Nothing to do yet re: rendering templates. Everything is untemplated so far.
11
- []
10
+ [
11
+ Generators::RobotsTxtGenerator,
12
+ Generators::RoutesTsGenerator
13
+ ]
12
14
  else
13
15
  # :nocov:
14
16
  raise "Not sure how build a generator for a #{manifest}"
@@ -16,6 +18,10 @@ module Foobara
16
18
  end
17
19
  end
18
20
  end
21
+
22
+ alias empty_typescript_react_project_config relevant_manifest
23
+
24
+ def templates_dir = "#{__dir__}/../templates"
19
25
  end
20
26
  end
21
27
  end
@@ -18,10 +18,7 @@ module Foobara
18
18
  add_initial_elements_to_generate
19
19
 
20
20
  each_element_to_generate do
21
- # currently there are no templated files to write and so this code path isn't hit
22
- # :nocov:
23
21
  generate_element
24
- # :nocov:
25
22
  end
26
23
 
27
24
  paths_to_source_code
@@ -0,0 +1,13 @@
1
+ require_relative "../empty_typescript_react_project_generator"
2
+
3
+ module Foobara
4
+ module Generators
5
+ module EmptyTypescriptReactProjectGenerator
6
+ module Generators
7
+ class RobotsTxtGenerator < EmptyTypescriptReactProjectGenerator
8
+ def template_path = ["public", "robots.txt.erb"]
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require_relative "../empty_typescript_react_project_generator"
2
+
3
+ module Foobara
4
+ module Generators
5
+ module EmptyTypescriptReactProjectGenerator
6
+ module Generators
7
+ class RoutesTsGenerator < EmptyTypescriptReactProjectGenerator
8
+ def template_path = ["scripts", "routes.ts.erb"]
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -56,7 +56,6 @@ module Foobara
56
56
  def run_pre_generation_tasks
57
57
  run_npm_create_vite
58
58
  npm_install
59
- eslint_fix
60
59
  git_init
61
60
  git_add_all
62
61
  git_commit_initial_setup
@@ -85,7 +84,11 @@ module Foobara
85
84
  end
86
85
 
87
86
  def run_post_generation_tasks
88
- eslint_fix
87
+ npm_install_react_router
88
+ npm_install_sitemap_and_prerender_dependencies
89
+ add_build_scripts_to_package_json
90
+ remove_static_title_from_index_html
91
+ oxlint_fix
89
92
  git_add_all
90
93
  git_commit_generated_files
91
94
  git_branch_main
@@ -97,10 +100,52 @@ module Foobara
97
100
  end
98
101
  end
99
102
 
100
- def eslint_fix
103
+ def npm_install_react_router
104
+ puts "installing react-router..."
105
+
106
+ cmd = "npm install react-router-dom"
107
+
108
+ Dir.chdir project_directory do
109
+ run_cmd_and_write_output(cmd)
110
+ end
111
+ end
112
+
113
+ def npm_install_sitemap_and_prerender_dependencies
114
+ puts "installing sitemap/pre-render dependencies..."
115
+
116
+ cmd = "npm install --save-dev puppeteer tsx @types/node"
117
+
118
+ Dir.chdir project_directory do
119
+ run_cmd_and_write_output(cmd)
120
+ end
121
+ end
122
+
123
+ def add_build_scripts_to_package_json
124
+ puts "adding prebuild/postbuild scripts..."
125
+
126
+ Dir.chdir project_directory do
127
+ run_cmd_and_write_output(
128
+ 'npm pkg set scripts.prebuild="npx puppeteer browsers install chrome && ' \
129
+ 'tsx scripts/generate-sitemap.ts"'
130
+ )
131
+ run_cmd_and_write_output(
132
+ 'npm pkg set scripts.postbuild="tsx scripts/prerender.ts"'
133
+ )
134
+ end
135
+ end
136
+
137
+ # Pages now set their own <title> in React 19
138
+ def remove_static_title_from_index_html
139
+ index_html_path = File.join(project_directory, "index.html")
140
+ contents = File.read(index_html_path)
141
+
142
+ File.write(index_html_path, contents.sub(%r{\s*<title>.*</title>}, ""))
143
+ end
144
+
145
+ def oxlint_fix
101
146
  puts "linting..."
102
147
 
103
- cmd = "npx eslint . --fix"
148
+ cmd = "npx oxlint --fix"
104
149
  Dir.chdir project_directory do
105
150
  run_cmd_and_write_output(cmd)
106
151
  end
@@ -0,0 +1,17 @@
1
+ {
2
+ "$schema": "./node_modules/oxlint/configuration_schema.json",
3
+ "plugins": ["react", "typescript", "oxc"],
4
+ "rules": {
5
+ "react/rules-of-hooks": "error",
6
+ "react/only-export-components": ["warn", { "allowConstantExport": true }]
7
+ },
8
+ "overrides": [
9
+ {
10
+ "files": ["src/domains/**/*.{ts,tsx}"],
11
+ "rules": {
12
+ "typescript/no-explicit-any": "off",
13
+ "typescript/prefer-as-const": "off"
14
+ }
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1 @@
1
+ /* /index.html 200
@@ -0,0 +1,5 @@
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
4
+
5
+ Sitemap: <%= site_url %>/sitemap.xml
@@ -0,0 +1,28 @@
1
+ import { writeFileSync } from 'fs'
2
+ import { resolve, dirname } from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { SITE_URL, getSitemapPages } from './routes.ts'
5
+
6
+ const __filename = fileURLToPath(import.meta.url)
7
+ const __dirname = dirname(__filename)
8
+
9
+ const allPages = getSitemapPages()
10
+
11
+ const sitemap = `<?xml version="1.0" encoding="UTF-8"?>
12
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
13
+ ${allPages
14
+ .map(
15
+ (page) => ` <url>
16
+ <loc>${SITE_URL}${page.loc}</loc>
17
+ <changefreq>${page.changefreq}</changefreq>
18
+ <priority>${page.priority}</priority>${page.lastmod ? `\n <lastmod>${page.lastmod}</lastmod>` : ''}
19
+ </url>`,
20
+ )
21
+ .join('\n')}
22
+ </urlset>
23
+ `
24
+
25
+ const sitemapPath = resolve(__dirname, '../public/sitemap.xml')
26
+ writeFileSync(sitemapPath, sitemap)
27
+
28
+ console.log(`Generated sitemap with ${allPages.length} URLs`)
@@ -0,0 +1,53 @@
1
+ import { writeFileSync, mkdirSync } from 'fs'
2
+ import { resolve, dirname } from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { preview } from 'vite'
5
+ import puppeteer from 'puppeteer'
6
+ import { getRoutePaths } from './routes.ts'
7
+
8
+ const __filename = fileURLToPath(import.meta.url)
9
+ const __dirname = dirname(__filename)
10
+ const projectRoot = resolve(__dirname, '..')
11
+ const distDir = resolve(projectRoot, 'dist')
12
+
13
+ const routes = getRoutePaths()
14
+
15
+ const server = await preview({ root: projectRoot })
16
+ const serverUrl = server.resolvedUrls!.local[0]
17
+
18
+ const browser = await puppeteer.launch({
19
+ args: ['--no-sandbox', '--disable-setuid-sandbox'],
20
+ })
21
+
22
+ // Render everything before writing anything to deal with / prerendering
23
+ const rendered: { route: string; outputPath: string; html: string }[] = []
24
+
25
+ for (const route of routes) {
26
+ const page = await browser.newPage()
27
+
28
+ await page.evaluateOnNewDocument(() => {
29
+ document.addEventListener('prerender-ready', () => {
30
+ ;(window as Window & { __prerenderReady?: boolean }).__prerenderReady = true
31
+ })
32
+ })
33
+
34
+ await page.goto(new URL(route, serverUrl).href, { waitUntil: 'networkidle0' })
35
+ await page.waitForFunction('window.__prerenderReady === true')
36
+
37
+ const outputPath = route === '/' ? 'index.html' : `${route.replace(/^\//, '')}.html`
38
+ rendered.push({ route, outputPath, html: await page.content() })
39
+
40
+ await page.close()
41
+ }
42
+
43
+ for (const { route, outputPath, html } of rendered) {
44
+ const outputFile = resolve(distDir, outputPath)
45
+
46
+ mkdirSync(dirname(outputFile), { recursive: true })
47
+ writeFileSync(outputFile, html)
48
+ console.log(`Pre-rendered ${route} -> dist/${outputPath}`)
49
+ }
50
+
51
+ await browser.close()
52
+ server.httpServer.close()
53
+ process.exit(0)
@@ -0,0 +1,25 @@
1
+ export const SITE_URL = '<%= site_url %>'
2
+
3
+ export interface SitemapPage {
4
+ loc: string
5
+ changefreq: string
6
+ priority: string
7
+ lastmod?: string
8
+ }
9
+
10
+ export const STATIC_PAGES: SitemapPage[] = [
11
+ { loc: '/', changefreq: 'monthly', priority: '1.0' },
12
+ { loc: '/about', changefreq: 'monthly', priority: '0.8' },
13
+ ]
14
+
15
+ export function getDynamicPages(): SitemapPage[] {
16
+ return []
17
+ }
18
+
19
+ export function getSitemapPages(): SitemapPage[] {
20
+ return [...STATIC_PAGES, ...getDynamicPages()]
21
+ }
22
+
23
+ export function getRoutePaths(): string[] {
24
+ return getSitemapPages().map((page) => page.loc)
25
+ }
@@ -0,0 +1,24 @@
1
+ import { NavLink, Route, Routes } from 'react-router-dom'
2
+ import './App.css'
3
+ import Home from './pages/Home'
4
+ import About from './pages/About'
5
+ import NotFound from './pages/NotFound'
6
+
7
+ function App() {
8
+ return (
9
+ <div className="App">
10
+ <nav>
11
+ <NavLink to="/">Home</NavLink> | <NavLink to="/about">About</NavLink>
12
+ </nav>
13
+ <main>
14
+ <Routes>
15
+ <Route path="/" element={<Home />} />
16
+ <Route path="/about" element={<About />} />
17
+ <Route path="*" element={<NotFound />} />
18
+ </Routes>
19
+ </main>
20
+ </div>
21
+ )
22
+ }
23
+
24
+ export default App
@@ -0,0 +1,28 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot, hydrateRoot } from 'react-dom/client'
3
+ import { BrowserRouter } from 'react-router-dom'
4
+ import './index.css'
5
+ import App from './App.tsx'
6
+
7
+ const rootElement = document.getElementById('root')!
8
+
9
+ const app = (
10
+ <StrictMode>
11
+ <BrowserRouter>
12
+ <App />
13
+ </BrowserRouter>
14
+ </StrictMode>
15
+ )
16
+
17
+ if (rootElement.hasChildNodes()) {
18
+ hydrateRoot(rootElement, app)
19
+ } else {
20
+ createRoot(rootElement).render(app)
21
+ }
22
+
23
+ // Not sure if this is necessary but gives some extra time for things to settle before pre-rendering
24
+ requestAnimationFrame(() => {
25
+ requestAnimationFrame(() => {
26
+ document.dispatchEvent(new Event('prerender-ready'))
27
+ })
28
+ })
@@ -0,0 +1,12 @@
1
+ function About() {
2
+ return (
3
+ <div>
4
+ <title>About</title>
5
+ <meta name="description" content="About this project" />
6
+ <h1>About</h1>
7
+ <p>Stuff about this project or whatever</p>
8
+ </div>
9
+ )
10
+ }
11
+
12
+ export default About
@@ -0,0 +1,12 @@
1
+ function Home() {
2
+ return (
3
+ <div>
4
+ <title>Home</title>
5
+ <meta name="description" content="The home page" />
6
+ <h1>Home</h1>
7
+ <p>Hello, World!</p>
8
+ </div>
9
+ )
10
+ }
11
+
12
+ export default Home
@@ -0,0 +1,11 @@
1
+ function NotFound() {
2
+ return (
3
+ <div>
4
+ <title>Not Found</title>
5
+ <h1>Not Found</h1>
6
+ <p>There is nothing at this URL.</p>
7
+ </div>
8
+ )
9
+ }
10
+
11
+ export default NotFound
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foobara-empty-typescript-react-project-generator
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi
@@ -58,10 +58,22 @@ files:
58
58
  - src/empty_typescript_react_project_config.rb
59
59
  - src/empty_typescript_react_project_generator.rb
60
60
  - src/generate_empty_typescript_react_project.rb
61
+ - src/generators/robots_txt_generator.rb
62
+ - src/generators/routes_ts_generator.rb
61
63
  - src/write_empty_typescript_react_project_to_disk.rb
62
64
  - templates/.env
63
65
  - templates/.github/workflows/tests.yml
64
- - templates/eslint.config.js
66
+ - templates/.oxlintrc.json
67
+ - templates/public/_redirects
68
+ - templates/public/robots.txt.erb
69
+ - templates/scripts/generate-sitemap.ts
70
+ - templates/scripts/prerender.ts
71
+ - templates/scripts/routes.ts.erb
72
+ - templates/src/App.tsx
73
+ - templates/src/main.tsx
74
+ - templates/src/pages/About.tsx
75
+ - templates/src/pages/Home.tsx
76
+ - templates/src/pages/NotFound.tsx
65
77
  homepage: https://github.com/foobara/generators-empty-typescript-react-project-generator
66
78
  licenses:
67
79
  - Apache-2.0
@@ -79,13 +91,16 @@ required_ruby_version: !ruby/object:Gem::Requirement
79
91
  - - ">="
80
92
  - !ruby/object:Gem::Version
81
93
  version: 3.4.0
94
+ - - "<"
95
+ - !ruby/object:Gem::Version
96
+ version: 4.1.0
82
97
  required_rubygems_version: !ruby/object:Gem::Requirement
83
98
  requirements:
84
99
  - - ">="
85
100
  - !ruby/object:Gem::Version
86
101
  version: '0'
87
102
  requirements: []
88
- rubygems_version: 3.6.9
103
+ rubygems_version: 4.0.10
89
104
  specification_version: 4
90
105
  summary: Generates empty typescript react projects
91
106
  test_files: []
@@ -1,30 +0,0 @@
1
- import js from '@eslint/js'
2
- import globals from 'globals'
3
- import reactHooks from 'eslint-plugin-react-hooks'
4
- import reactRefresh from 'eslint-plugin-react-refresh'
5
- import tseslint from 'typescript-eslint'
6
- import { defineConfig, globalIgnores } from 'eslint/config'
7
-
8
- export default defineConfig([
9
- globalIgnores(['dist']),
10
- {
11
- files: ['**/*.{ts,tsx}'],
12
- extends: [
13
- js.configs.recommended,
14
- tseslint.configs.recommended,
15
- reactHooks.configs.flat.recommended,
16
- reactRefresh.configs.vite,
17
- ],
18
- languageOptions: {
19
- ecmaVersion: 2020,
20
- globals: globals.browser,
21
- },
22
- },
23
- {
24
- files: ['src/domains/**/*.{ts,tsx}'],
25
- rules: {
26
- '@typescript-eslint/no-explicit-any': 'off',
27
- '@typescript-eslint/prefer-as-const': 'off',
28
- },
29
- },
30
- ])