Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

feat: create initial TS template #868

Merged
merged 11 commits into from
Oct 8, 2024
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 = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this file is needed -- these are more JS code to import as defaults, rather than boilerplate to be copied into a new app

(I actually meant to add a comment about this when separating these objects from the boilerplate files, but forgot to add it in the last PR, so it's in a more recent one: d2ConfigDefaults.js)

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.
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 })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Random brainstorming that I think is out of scope for this PR:] It would be cool if passing the ts flag here wasn't necessary, but I think that would need a refactoring of the paths to use something like like globs, which may end up being more complex


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: {
Copy link
Contributor

@KaiVandivier KaiVandivier Aug 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suuuper subjective and equally minor since the alias is set up, and maybe I’m crazy, but I’ll share in case it’s in the back of your mind too: --typeScript feels weird to me as a flag or as a variable name 😅 I think of it as typescript or ts — it seems like you even used --typescript too when you wrote out CLI command script for your workflow

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
Loading