Skip to content

Commit

Permalink
feat: create initial TS template (#868)
Browse files Browse the repository at this point in the history
* feat: create initial TS template

* fix: update start command to work with TS

* fix: update init to accept command line arguments

* fix: update makePaths to work when no options are passed

* fix: update eslint rules for new template

* fix: update init command to cater for TS project configurations

- add custom declaration files for React and CSS modules
- install relevant type definitions: react, jest, etc
- add initial eslint config file

* fix: move init files into templates directory

- JS files reside in init dir
- TS files reside init-typescript dir
- Common dir contains files used by both templates, i.e. README.md, package.json and App.module.css files to reduce duplication
- update eslintignore file with new file paths

* fix: modify base eslint config and install eslint dependencies

* fix: remove type from package.json

* chore: dedupe yarn.lock

---------

Co-authored-by: Diana Nanyanzi <diana@dhis2.org>
  • Loading branch information
kabaros and d-rita authored Oct 8, 2024
1 parent 521f483 commit 2795f79
Show file tree
Hide file tree
Showing 24 changed files with 940 additions and 1,478 deletions.
9 changes: 7 additions & 2 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
/cli/assets
**/locales/index.js
# These are to avoid lint errors like 'cannot find module App.jsx'
cli/config/init/entrypoint.jsx
cli/config/init/App.test.jsx
cli/config/templates/init/entrypoint.jsx
cli/config/templates/init/App.test.jsx
cli/config/templates/init-typescript/entrypoint.tsx
cli/config/templates/init-typescript/App.test.tsx
cli/config/templates/init-typescript/global.d.ts
cli/config/templates/init-typescript/modules.d.ts
cli/config/templates/init-typescript/eslint.config.js
68 changes: 68 additions & 0 deletions cli/config/d2ConfigDefaults.typescript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
const defaultsApp = {
type: 'app',

entryPoints: {
app: './src/App.tsx',
},
}

const defaultsLib = {
type: 'lib',

entryPoints: {
lib: './src/index.tsx',
},
}

const defaultsPWA = {
pwa: {
/**
* If true, service worker is registered to perform offline caching
* and use of cacheable sections & recording mode is enabled
*/
enabled: false,
caching: {
/**
* If true, don't cache requests to exteral domains in app shell.
* Doesn't affect recording mode
*/
omitExternalRequestsFromAppShell: false,
/** Deprecated version of above */
omitExternalRequests: false,
/**
* Don't cache URLs matching patterns in this array in app shell.
* Doesn't affect recording mode
*/
patternsToOmitFromAppShell: [],
/** Deprecated version of above */
patternsToOmit: [],
/**
* Don't cache URLs matching these patterns in recorded sections.
* Can still be cached in app shell unless filtered there too.
*/
patternsToOmitFromCacheableSections: [],
/**
* In addition to the contents of an app's 'build' folder, other
* URLs can be precached by adding them to this list which will
* add them to the precache manifest at build time. The format of
* this list must match the Workbox precache list format:
* https://developers.google.com/web/tools/workbox/modules/workbox-precaching#explanation_of_the_precache_list
*/
additionalManifestEntries: [],
/**
* By default, all the contents of the `build` folder are added to
* the precache to give the app the best chances of functioning
* completely while offline. Developers may choose to omit some
* of these files (for example, thousands of font or image files)
* if they cause cache bloat and the app can work fine without
* them precached. See LIBS-482
*
* The globs should be relative to the public dir of the built app.
* Used in injectPrecacheManifest.js
*/
globsToOmitFromPrecache: [],
},
},
}

module.exports = { defaultsApp, defaultsLib, defaultsPWA }
9 changes: 0 additions & 9 deletions cli/config/init/App.module.css

This file was deleted.

9 changes: 9 additions & 0 deletions cli/config/templates/common/App.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.container {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: 1rem;
}
File renamed without changes.
File renamed without changes.
20 changes: 20 additions & 0 deletions cli/config/templates/init-typescript/App.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { CustomDataProvider } from '@dhis2/app-runtime'
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

it('renders without crashing', () => {
const div = document.createElement('div')

const data = {
resource: 'test',
}

ReactDOM.render(
<CustomDataProvider data={data}>
<App />
</CustomDataProvider>,
div
)
ReactDOM.unmountComponentAtNode(div)
})
9 changes: 9 additions & 0 deletions cli/config/templates/init-typescript/d2.config.app.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const config = {
type: 'app',

entryPoints: {
app: './src/App.tsx',
},
}

module.exports = config
9 changes: 9 additions & 0 deletions cli/config/templates/init-typescript/d2.config.lib.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const config = {
type: 'lib',

entryPoints: {
lib: './src/index.tsx',
},
}

module.exports = config
37 changes: 37 additions & 0 deletions cli/config/templates/init-typescript/entrypoint.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useDataQuery } from '@dhis2/app-runtime'
import i18n from '@dhis2/d2-i18n'
import React, { FC } from 'react'
import classes from './App.module.css'

interface QueryResults {
me: {
name: string
}
}

const query = {
me: {
resource: 'me',
},
}

const MyApp: FC = () => {
const { error, loading, data } = useDataQuery<QueryResults>(query)

if (error) {
return <span>{i18n.t('ERROR')}</span>
}

if (loading) {
return <span>{i18n.t('Loading...')}</span>
}

return (
<div className={classes.container}>
<h1>{i18n.t('Hello {{name}}', { name: data?.me?.name })}</h1>
<h3>{i18n.t('Welcome to DHIS2 with TypeScript!')}</h3>
</div>
)
}

export default MyApp
17 changes: 17 additions & 0 deletions cli/config/templates/init-typescript/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import js from '@eslint/js'
import tseslint from 'typescript-eslint'

export default [
js.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ['*.d.ts', 'd2.config.js'],
},
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
},
]
8 changes: 8 additions & 0 deletions cli/config/templates/init-typescript/global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import 'react'

declare module 'react' {
interface StyleHTMLAttributes<T> extends React.HTMLAttributes<T> {
jsx?: boolean
global?: boolean
}
}
4 changes: 4 additions & 0 deletions cli/config/templates/init-typescript/modules.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
declare module '*.module.css' {
const classes: { [key: string]: string }
export default classes
}
13 changes: 13 additions & 0 deletions cli/config/templates/init-typescript/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"noEmit": true,
"skipLibCheck": true,
"allowJs": true,
"jsx": "react",
"esModuleInterop": true,
"target": "ESNext",
"module": "esnext",
"moduleResolution": "node"
},
"include": ["src", "types"]
}
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
1 change: 1 addition & 0 deletions cli/src/commands/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ const handler = async ({
pack: packAppOutput,
allowJsxInJs,
}) => {
// todo: we need to infer TypeScript in build command here similar to start
const paths = makePaths(cwd)

mode = mode || (dev && 'development') || getNodeEnv() || 'production'
Expand Down
86 changes: 81 additions & 5 deletions cli/src/commands/init.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,25 @@ const writeGitignore = (gitignoreFile, sections) => {
fs.writeFileSync(gitignoreFile, gitignore.stringify(sections, format))
}

const handler = async ({ force, name, cwd, lib }) => {
const handler = async ({ force, name, cwd, lib, typeScript }) => {
// create the folder where the template will be generated
cwd = cwd || process.cwd()
cwd = path.join(cwd, name)
fs.mkdirpSync(cwd)
const paths = makePaths(cwd)
const paths = makePaths(cwd, { typeScript })

reporter.info('checking d2.config exists')
if (fs.existsSync(paths.config) && !force) {
reporter.warn(
'A config file already exists, use --force to overwrite it'
)
} else {
reporter.info('Importing d2.config.js defaults')
reporter.info('Importing d2.config defaults')
fs.copyFileSync(
lib ? paths.initConfigLib : paths.initConfigApp,
paths.config
)
reporter.debug(' copied default d2.config')
}

if (!fs.existsSync(paths.package)) {
Expand Down Expand Up @@ -183,7 +186,74 @@ const handler = async ({ force, name, cwd, lib }) => {
})
}

const entrypoint = lib ? 'src/index.jsx' : 'src/App.jsx'
if (typeScript) {
// copy tsconfig
reporter.info('Copying tsconfig')
fs.copyFileSync(paths.initTSConfig, paths.tsConfig)

reporter.info('install TypeScript as a dev dependency')
// ToDO: restrict the major version of TS we install
await exec({
cmd: 'yarn',
args: ['add', 'typescript', '--dev'],
cwd: paths.base,
})

// install any other TS dependencies needed
reporter.info('install type definitions')
await exec({
cmd: 'yarn',
args: [
'add',
'@types/react @types/react-dom @types/jest',
'@types/eslint',
'--dev',
],
cwd: paths.base,
})

// add global.d.ts to get rid of CSS module errors
// something like https://github.com/dhis2/data-exchange-app/pull/79/files#diff-858566d2d4cf06579a908cb85f587c5752fa0fa6a47d579277749006e86f0834
// (but maybe something better)
// also look at copying src/custom.d.ts https://github.com/dhis2/data-exchange-app/pull/79/files#diff-5f2ca1b1541dc3023f81543689da349e59b97c708462dd8da4640b399362edc7
reporter.info('add declaration files')
const typesDir = path.join(paths.base, 'types')

if (!fs.existsSync(typesDir)) {
fs.mkdirpSync(typesDir)
}
fs.copyFileSync(
paths.initGlobalDeclaration,
path.join(typesDir, 'global.d.ts')
)
fs.copyFileSync(
paths.initModulesDeclaration,
path.join(typesDir, 'modules.d.ts')
)

// ToDO: make custom eslint config part of the template (and copy it)
// similar to: https://github.com/dhis2/data-exchange-app/pull/79/files#diff-e2954b558f2aa82baff0e30964490d12942e0e251c1aa56c3294de6ec67b7cf5
// install dependencies needed for eslint
// "@typescript-eslint/eslint-plugin"
// "@typescript-eslint/parser"

reporter.info('setting up eslint configuration')
await exec({
cmd: 'yarn',
args: ['add', 'eslint @eslint/js typescript-eslint', '--dev'],
cwd: paths.base,
})
// copy eslint config
fs.copyFileSync(paths.initEslint, paths.eslintConfig)

// ToDO: we're hardcoding running TS, we need to figure out how to pass the argument from the CLI

// ToDO: aim to have a TS project that runs with "yarn start" and "yarn build"
}

const extension = typeScript ? 'ts' : 'js'

const entrypoint = lib ? `src/index.${extension}x` : `src/App.${extension}x`

if (fs.existsSync(path.join(paths.base, entrypoint))) {
reporter.warn(
Expand All @@ -197,7 +267,7 @@ const handler = async ({ force, name, cwd, lib }) => {
if (!lib) {
fs.copyFileSync(
paths.initAppTestJsx,
path.join(paths.base, 'src/App.test.jsx')
path.join(paths.base, `src/App.test.${extension}x`)
)
fs.copyFileSync(
paths.initAppModuleCss,
Expand Down Expand Up @@ -242,6 +312,12 @@ const command = {
type: 'boolean',
default: false,
},
typeScript: {
alias: ['typescript', 'ts'],
description: 'Use TypeScript template',
type: 'boolean',
default: false,
},
},
handler,
}
Expand Down
13 changes: 12 additions & 1 deletion cli/src/commands/start.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const path = require('path')
const { reporter, chalk } = require('@dhis2/cli-helpers-engine')
const detectPort = require('detect-port')
const fs = require('fs-extra')
const bootstrapShell = require('../lib/bootstrapShell')
const { compile } = require('../lib/compiler')
const { loadEnvFiles, getEnv } = require('../lib/env')
Expand All @@ -25,7 +27,16 @@ const handler = async ({
host,
allowJsxInJs,
}) => {
const paths = makePaths(cwd)
// infer whether this is a TS project based on whether it contains a tsconfig
const typeScript = fs.existsSync(
path.join(cwd ?? process.cwd(), './tsconfig.json')
)

if (typeScript) {
reporter.debug('starting a TypeScript project')
}

const paths = makePaths(cwd, { typeScript })

const mode = 'development'
process.env.BABEL_ENV = process.env.NODE_ENV = mode
Expand Down
Loading

0 comments on commit 2795f79

Please sign in to comment.