Skip to content

Commit

Permalink
Add JSDoc annotations to templates (#4621)
Browse files Browse the repository at this point in the history
* Set checkJs to false in no-typescript scaffold

* [enhance] add selection for type checking to create-svelte

* Add changelog

* Adjust file inclusion logic to checkjs option

* add jsdoc annotations

* strip jsdoc from source

* tidy up

* oops

* Update packages/create-svelte/index.js

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>

* strip jsdoc annotations when no typechecking is selected

Co-authored-by: Theo Steiner <theosteiner@gmail.com>
Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
  • Loading branch information
3 people authored Apr 15, 2022
1 parent 4a2590f commit f445802
Show file tree
Hide file tree
Showing 17 changed files with 228 additions and 99 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-boats-wave.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'create-svelte': patch
---

Disable type checking by default for non-typescript projects.
18 changes: 12 additions & 6 deletions packages/create-svelte/bin.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,12 +70,15 @@ async function main() {
})
},
{
type: 'toggle',
name: 'typescript',
message: 'Use TypeScript?',
type: 'select',
name: 'types',
message: 'Add type checking?',
initial: false,
active: 'Yes',
inactive: 'No'
choices: [
{ title: 'Type-checked JavaScript', value: 'checkjs' },
{ title: 'TypeScript', value: 'typescript' },
{ title: 'None', value: null }
]
},
{
type: 'toggle',
Expand Down Expand Up @@ -116,9 +119,12 @@ async function main() {

console.log(bold(green('\nYour project is ready!')));

if (options.typescript) {
if (options.types === 'typescript') {
console.log(bold('✔ Typescript'));
console.log(' Inside Svelte components, use <script lang="ts">');
} else if (options.types === 'checkjs') {
console.log(bold('✔ Type-checked JavaScript'));
console.log(' https://www.typescriptlang.org/tsconfig#checkJs');
}

if (options.eslint) {
Expand Down
18 changes: 11 additions & 7 deletions packages/create-svelte/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,22 @@ import { mkdirp, copy, dist } from './utils.js';
export async function create(cwd, options) {
mkdirp(cwd);

write_template_files(options.template, options.typescript, options.name, cwd);
write_template_files(options.template, options.types, options.name, cwd);
write_common_files(cwd, options, options.name);
}

/**
* @param {string} template
* @param {boolean} typescript
* @param {'typescript' | 'checkjs' | null} types
* @param {string} name
* @param {string} cwd
*/
function write_template_files(template, typescript, name, cwd) {
function write_template_files(template, types, name, cwd) {
const dir = dist(`templates/${template}`);
copy(`${dir}/assets`, cwd, (name) => name.replace('DOT-', '.'));
copy(`${dir}/package.json`, `${cwd}/package.json`);

const manifest = `${dir}/files.${typescript ? 'ts' : 'js'}.json`;
const manifest = `${dir}/files.types=${types}.json`;
const files = /** @type {import('./types/internal').File[]} */ (
JSON.parse(fs.readFileSync(manifest, 'utf-8'))
);
Expand Down Expand Up @@ -83,9 +83,13 @@ function write_common_files(cwd, options, name) {
* @returns {boolean}
*/
function matches_condition(condition, options) {
return condition === 'default' || condition === 'skeleton'
? options.template === condition
: options[condition];
if (condition === 'default' || condition === 'skeleton') {
return options.template === condition;
}
if (condition === 'typescript' || condition === 'checkjs') {
return options.types === condition;
}
return options[condition];
}

/**
Expand Down
179 changes: 101 additions & 78 deletions packages/create-svelte/scripts/build-templates.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import { transform } from 'sucrase';
import glob from 'tiny-glob/sync.js';
import { mkdirp, rimraf } from '../utils.js';

/** @param {string} typescript */
function convert_typescript(typescript) {
const transformed = transform(typescript, {
/** @param {string} content */
function convert_typescript(content) {
const transformed = transform(content, {
transforms: ['typescript']
});

Expand All @@ -21,6 +21,11 @@ function convert_typescript(typescript) {
});
}

/** @param {string} content */
function strip_jsdoc(content) {
return content.replace(/\/\*\*[\s\S]+?\*\/[\s\n]+/g, '');
}

/** @param {Set<string>} shared */
async function generate_templates(shared) {
const templates = fs.readdirSync('templates');
Expand All @@ -43,8 +48,12 @@ async function generate_templates(shared) {
const meta_file = path.join(cwd, '.meta.json');
if (!fs.existsSync(meta_file)) throw new Error('Template must have a .meta.json file');

/** @type {import('../types/internal.js').File[]} */
const ts = [];
/** @type {Record<string, import('../types/internal.js').File[]>} */
const types = {
typescript: [],
checkjs: [],
null: []
};

glob('**/*', { cwd, filesOnly: true, dot: true }).forEach((name) => {
// the package.template.json thing is a bit annoying — basically we want
Expand All @@ -64,88 +73,102 @@ async function generate_templates(shared) {
// ignore contents of .gitignore or .ignore
if (!gitignore.accepts(name) || !ignore.accepts(name) || name === '.ignore') return;

if (/\.(js|ts|svelte|svelte\.md)$/.test(name)) {
if (/\.(ts|svelte)$/.test(name)) {
const contents = fs.readFileSync(path.join(cwd, name), 'utf8');
ts.push({
name,
contents
});

if (name.endsWith('.d.ts')) {
if (name.endsWith('app.d.ts')) types.checkjs.push({ name, contents });
types.typescript.push({ name, contents });
} else if (name.endsWith('.ts')) {
const js = convert_typescript(contents);

types.typescript.push({
name,
contents: strip_jsdoc(contents)
});

types.checkjs.push({
name: name.replace(/\.ts$/, '.js'),
contents: js
});

types.null.push({
name: name.replace(/\.ts$/, '.js'),
contents: strip_jsdoc(js)
});
} else {
// we jump through some hoops, rather than just using svelte.preprocess,
// so that the output preserves the original formatting to the extent
// possible (e.g. preserving double line breaks). Sucrase is the best
// tool for the job because it just removes the types; Prettier then
// tidies up the end result
const js_contents = contents.replace(
/<script([^>]+)>([\s\S]+?)<\/script>/g,
(m, attrs, typescript) => {
// Sucrase assumes 'unused' imports (which _are_ used, but only
// in the markup) are type imports, and strips them. This step
// prevents it from drawing that conclusion
const imports = [];
const import_pattern = /import (.+?) from/g;
let import_match;
while ((import_match = import_pattern.exec(typescript))) {
const word_pattern = /[a-z_$][a-z0-9_$]*/gi;
let word_match;
while ((word_match = word_pattern.exec(import_match[1]))) {
imports.push(word_match[0]);
}
}

const suffix = `\n${imports.join(',')}`;

const transformed = transform(typescript + suffix, {
transforms: ['typescript']
}).code.slice(0, -suffix.length);

const contents = prettier
.format(transformed, {
parser: 'babel',
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 100
})
.trim()
.replace(/^(.)/gm, '\t$1');

return `<script${attrs.replace(' lang="ts"', '')}>\n${contents}\n</script>`;
}
);

types.typescript.push({
name,
contents: strip_jsdoc(contents)
});

types.checkjs.push({
name,
contents: js_contents
});

types.null.push({
name,
contents: strip_jsdoc(js_contents)
});
}
} else {
const dest = path.join(assets, name.replace(/^\./, 'DOT-'));
mkdirp(path.dirname(dest));
fs.copyFileSync(path.join(cwd, name), dest);
}
});

/** @type {import('../types/internal.js').File[]} */
const js = [];

for (const file of ts) {
// The app.d.ts file makes TS/JS aware of some ambient modules, which are
// also needed for JS projects if people turn on "checkJs" in their jsonfig
if (file.name.endsWith('.d.ts')) {
if (file.name.endsWith('app.d.ts')) js.push(file);
} else if (file.name.endsWith('.ts')) {
js.push({
name: file.name.replace(/\.ts$/, '.js'),
contents: convert_typescript(file.contents)
});
} else if (file.name.endsWith('.svelte')) {
// we jump through some hoops, rather than just using svelte.preprocess,
// so that the output preserves the original formatting to the extent
// possible (e.g. preserving double line breaks). Sucrase is the best
// tool for the job because it just removes the types; Prettier then
// tidies up the end result
const contents = file.contents.replace(
/<script([^>]+)>([\s\S]+?)<\/script>/g,
(m, attrs, typescript) => {
// Sucrase assumes 'unused' imports (which _are_ used, but only
// in the markup) are type imports, and strips them. This step
// prevents it from drawing that conclusion
const imports = [];
const import_pattern = /import (.+?) from/g;
let import_match;
while ((import_match = import_pattern.exec(typescript))) {
const word_pattern = /[a-z_$][a-z0-9_$]*/gi;
let word_match;
while ((word_match = word_pattern.exec(import_match[1]))) {
imports.push(word_match[0]);
}
}

const suffix = `\n${imports.join(',')}`;

const transformed = transform(typescript + suffix, {
transforms: ['typescript']
}).code.slice(0, -suffix.length);

const contents = prettier
.format(transformed, {
parser: 'babel',
useTabs: true,
singleQuote: true,
trailingComma: 'none',
printWidth: 100
})
.trim()
.replace(/^(.)/gm, '\t$1');

return `<script${attrs.replace(' lang="ts"', '')}>\n${contents}\n</script>`;
}
);

js.push({
name: file.name,
contents
});
} else {
js.push(file);
}
}

fs.copyFileSync(meta_file, `${dir}/meta.json`);
fs.writeFileSync(`${dir}/files.ts.json`, JSON.stringify(ts, null, '\t'));
fs.writeFileSync(`${dir}/files.js.json`, JSON.stringify(js, null, '\t'));
fs.writeFileSync(
`${dir}/files.types=typescript.json`,
JSON.stringify(types.typescript, null, '\t')
);
fs.writeFileSync(`${dir}/files.types=checkjs.json`, JSON.stringify(types.checkjs, null, '\t'));
fs.writeFileSync(`${dir}/files.types=null.json`, JSON.stringify(types.null, null, '\t'));
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ await create(repo, {
name: 'kit-template-default',
template: 'default',
eslint: false,
typescript: false,
types: 'checkjs',
prettier: true,
playwright: false
});
6 changes: 6 additions & 0 deletions packages/create-svelte/shared/+checkjs/jsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"extends": "./.svelte-kit/tsconfig.json",
"compilerOptions": {
"checkJs": true
}
}
10 changes: 10 additions & 0 deletions packages/create-svelte/shared/+checkjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"scripts": {
"check": "svelte-check --tsconfig ./jsconfig.json",
"check:watch": "svelte-check --tsconfig ./jsconfig.json --watch"
},
"devDependencies": {
"typescript": "~4.6.2",
"svelte-check": "^2.2.6"
}
}
5 changes: 5 additions & 0 deletions packages/create-svelte/shared/+default+checkjs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"devDependencies": {
"@types/cookie": "^0.4.1"
}
}
20 changes: 20 additions & 0 deletions packages/create-svelte/shared/+default+checkjs/svelte.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import adapter from '@sveltejs/adapter-auto';
import preprocess from 'svelte-preprocess';

/** @type {import('@sveltejs/kit').Config} */
const config = {
// Consult https://github.com/sveltejs/svelte-preprocess
// for more information about preprocessors
preprocess: preprocess(),

kit: {
adapter: adapter(),

// Override http methods in the Todo forms
methodOverride: {
allowed: ['PATCH', 'DELETE']
}
}
};

export default config;
3 changes: 0 additions & 3 deletions packages/create-svelte/shared/-typescript/jsconfig.json

This file was deleted.

1 change: 1 addition & 0 deletions packages/create-svelte/templates/default/src/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import cookie from 'cookie';
import { v4 as uuid } from '@lukeed/uuid';
import type { Handle } from '@sveltejs/kit';

/** @type {import('@sveltejs/kit').Handle} */
export const handle: Handle = async ({ event, resolve }) => {
const cookies = cookie.parse(event.request.headers.get('cookie') || '');
event.locals.userid = cookies.userid || uuid();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@
$: displayed_count.set(count);
$: offset = modulo($displayed_count, 1);
/**
* @param {number} n
* @param {number} m
*/
function modulo(n: number, m: number) {
// handle negative numbers
return ((n % m) + m) % m;
Expand Down
Loading

0 comments on commit f445802

Please sign in to comment.