From 0a85ae02233c2630fae37f7ef46e7b6376c277f0 Mon Sep 17 00:00:00 2001 From: Nhan Lam Date: Mon, 29 Apr 2024 07:25:50 +0700 Subject: [PATCH 1/3] Option for disable auto load individual CLI expansion --- .../cli-posts/102 - expansion/index.md | 5 ++- packages/cli/cli/utils/cli.ts | 31 ++++++++++--------- packages/project/lib/types/cli.ts | 2 +- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/apps/tinijs.dev/content/cli-posts/102 - expansion/index.md b/apps/tinijs.dev/content/cli-posts/102 - expansion/index.md index 734e8ed7..ee601822 100644 --- a/apps/tinijs.dev/content/cli-posts/102 - expansion/index.md +++ b/apps/tinijs.dev/content/cli-posts/102 - expansion/index.md @@ -24,7 +24,10 @@ export default defineTiniConfig({ 'some-name-2' ], - // disable autoload official and local expansions, default: false (auto load) + // disable autoload official and local expansions, default: false (auto load all available) + // undefined or false: auto load all available + // true: disable all auto load + // string[]: disable specific ones, example: ['local', '@tinijs/content'] noAutoExpansions: true, }, diff --git a/packages/cli/cli/utils/cli.ts b/packages/cli/cli/utils/cli.ts index bfb3c5bf..0ef73498 100644 --- a/packages/cli/cli/utils/cli.ts +++ b/packages/cli/cli/utils/cli.ts @@ -138,7 +138,7 @@ export async function setupCLIExpansion< const {expand: cliExpand = [], noAutoExpansions} = tiniProject.config.cli || {}; // auto load available expansions - if (!noAutoExpansions) { + if (noAutoExpansions !== true) { // official const {dependencies, devDependencies, peerDependencies} = await loadProjectPackageJSON(); @@ -150,24 +150,27 @@ export async function setupCLIExpansion< for (const officialPackage of OFFICIAL_EXPANSIONS) { if ( allDependencies[officialPackage] && - !isIntegratedItemExistsInConfig(cliExpand, officialPackage) + !isIntegratedItemExistsInConfig(cliExpand, officialPackage) && + (!noAutoExpansions || !noAutoExpansions.includes(officialPackage)) ) { cliExpand.push(officialPackage); } } // local - const autoTSFile = resolve('cli', 'expand.ts'); - const autoJSFile = tsToJS(autoTSFile); - if (pathExistsSync(autoTSFile)) { - const {default: localAuto} = (await jiti.import(autoTSFile, {})) as { - default: CLIExpansionConfig; - }; - if (localAuto) cliExpand.push(localAuto); - } else if (pathExistsSync(autoJSFile)) { - const {default: localAuto} = (await import(autoJSFile)) as { - default: CLIExpansionConfig; - }; - if (localAuto) cliExpand.push(localAuto); + if (!noAutoExpansions || !noAutoExpansions.includes('local')) { + const autoTSFile = resolve('cli', 'expand.ts'); + const autoJSFile = tsToJS(autoTSFile); + if (pathExistsSync(autoTSFile)) { + const {default: localAuto} = (await jiti.import(autoTSFile, {})) as { + default: CLIExpansionConfig; + }; + if (localAuto) cliExpand.push(localAuto); + } else if (pathExistsSync(autoJSFile)) { + const {default: localAuto} = (await import(autoJSFile)) as { + default: CLIExpansionConfig; + }; + if (localAuto) cliExpand.push(localAuto); + } } } // load expandable commands diff --git a/packages/project/lib/types/cli.ts b/packages/project/lib/types/cli.ts index c3f4c0f2..42656efe 100644 --- a/packages/project/lib/types/cli.ts +++ b/packages/project/lib/types/cli.ts @@ -33,7 +33,7 @@ export interface CLIConfig { module?: false; noBuiltins?: true; expand?: TiniIntegration; - noAutoExpansions?: true; + noAutoExpansions?: true | string[]; } export interface CLIHooks { From 9a73b228cdb234a6f932ecc2939256aa978ca5a5 Mon Sep 17 00:00:00 2001 From: Nhan Lam Date: Mon, 29 Apr 2024 10:44:20 +0700 Subject: [PATCH 2/3] Implement default compiler --- package-lock.json | 88 ++++++----- packages/cli/cli/commands/build.ts | 3 +- packages/cli/cli/commands/compile.ts | 44 ++++-- packages/cli/cli/commands/dev.ts | 3 +- packages/cli/cli/utils/build.ts | 58 +++++++- packages/cli/package.json | 2 +- packages/default-compiler/lib/index.ts | 194 +++++++++++++++++++------ packages/default-compiler/package.json | 4 +- packages/project/lib/types/build.ts | 2 +- packages/project/lib/types/compile.ts | 35 ++++- packages/project/lib/utils/dir.ts | 6 + 11 files changed, 340 insertions(+), 99 deletions(-) diff --git a/package-lock.json b/package-lock.json index 004db558..ccdbf50e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6934,7 +6934,6 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/@types/clean-css/-/clean-css-4.2.11.tgz", "integrity": "sha512-Y8n81lQVTAfP2TOdtJJEsCoYl1AnOkqDqMvXb9/7pfgZZ7r8YrEyurrAvAoAjHOGXKRybay+5CsExqIH6liccw==", - "dev": true, "dependencies": { "@types/node": "*", "source-map": "^0.6.0" @@ -7179,8 +7178,7 @@ "node_modules/@types/relateurl": { "version": "0.2.33", "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", - "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==", - "dev": true + "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==" }, "node_modules/@types/resolve": { "version": "1.20.2", @@ -7242,7 +7240,6 @@ "version": "3.17.5", "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.17.5.tgz", "integrity": "sha512-TU+fZFBTBcXj/GpDpDaBmgWk/gn96kMZ+uocaFUlV2f8a6WdMzzI44QBCmGcCiYR0Y6ZlNRiyUyKKt5nl/lbzQ==", - "dev": true, "dependencies": { "source-map": "^0.6.1" } @@ -16837,6 +16834,36 @@ "node": ">=4" } }, + "node_modules/minify-html-literals": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/minify-html-literals/-/minify-html-literals-1.3.5.tgz", + "integrity": "sha512-p8T8ryePRR8FVfJZLVFmM53WY25FL0moCCTycUDuAu6rf9GMLwy0gNjXBGNin3Yun7Y+tIWd28axOf0t2EpAlQ==", + "dependencies": { + "@types/html-minifier": "^3.5.3", + "clean-css": "^4.2.1", + "html-minifier": "^4.0.0", + "magic-string": "^0.25.0", + "parse-literals": "^1.2.1" + } + }, + "node_modules/minify-html-literals/node_modules/@types/html-minifier": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/@types/html-minifier/-/html-minifier-3.5.3.tgz", + "integrity": "sha512-j1P/4PcWVVCPEy5lofcHnQ6BtXz9tHGiFPWzqm7TtGuWZEfCHEP446HlkSNc9fQgNJaJZ6ewPtp2aaFla/Uerg==", + "dependencies": { + "@types/clean-css": "*", + "@types/relateurl": "*", + "@types/uglify-js": "*" + } + }, + "node_modules/minify-html-literals/node_modules/magic-string": { + "version": "0.25.9", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", + "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==", + "dependencies": { + "sourcemap-codec": "^1.4.8" + } + }, "node_modules/minimalistic-assert": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", @@ -18818,6 +18845,26 @@ "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" }, + "node_modules/parse-literals": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/parse-literals/-/parse-literals-1.2.1.tgz", + "integrity": "sha512-Ml0w104Ph2wwzuRdxrg9booVWsngXbB4bZ5T2z6WyF8b5oaNkUmBiDtahi34yUIpXD8Y13JjAK6UyIyApJ73RQ==", + "dependencies": { + "typescript": "^2.9.2 || ^3.0.0 || ^4.0.0" + } + }, + "node_modules/parse-literals/node_modules/typescript": { + "version": "4.9.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", + "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + }, "node_modules/parse-path": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-path/-/parse-path-7.0.0.tgz", @@ -25253,7 +25300,7 @@ "open": "^10.0.4", "ora": "^8.0.1", "pathe": "^1.1.2", - "picomatch": "^4.0.1", + "picomatch": "^4.0.2", "prettier": "^3.2.5", "recursive-readdir": "^2.2.3", "superstatic": "^9.0.3", @@ -25637,33 +25684,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "packages/content/node_modules/restore-cursor/node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "engines": { - "node": ">=6" - } - }, - "packages/content/node_modules/restore-cursor/node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "packages/content/node_modules/restore-cursor/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" - }, "packages/content/node_modules/string-width": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz", @@ -25731,8 +25751,10 @@ "@tinijs/cli": "^0.17.0", "@tinijs/project": "^0.17.0", "fs-extra": "^11.2.0", + "minify-html-literals": "^1.3.5", "pathe": "^1.1.2", - "sass": "^1.71.1" + "picomatch": "^4.0.2", + "sass": "^1.75.0" }, "engines": { "node": ">=18.0.0" diff --git a/packages/cli/cli/commands/build.ts b/packages/cli/cli/commands/build.ts index 4a27214e..043e5367 100644 --- a/packages/cli/cli/commands/build.ts +++ b/packages/cli/cli/commands/build.ts @@ -24,11 +24,10 @@ export const buildCommand = createCLICommand( }, }, async (args, callbacks) => { - const targetEnv = args.target || 'production'; const tiniProject = await getTiniProject(); const {config: tiniConfig, hooks} = tiniProject; // preparation - exposeEnvs(tiniConfig, targetEnv); + exposeEnvs(tiniConfig, args.target || 'production'); const compiler = await loadCompiler(tiniProject); const builder = await loadBuilder(tiniProject); // clean diff --git a/packages/cli/cli/commands/compile.ts b/packages/cli/cli/commands/compile.ts index 7d0f77cc..900a7376 100644 --- a/packages/cli/cli/commands/compile.ts +++ b/packages/cli/cli/commands/compile.ts @@ -3,10 +3,16 @@ import {resolve} from 'pathe'; import {consola} from 'consola'; import {green} from 'colorette'; import {remove} from 'fs-extra/esm'; +import picomatch from 'picomatch'; -import {getTiniProject} from '@tinijs/project'; +import {getTiniProject, getProjectDirs} from '@tinijs/project'; -import {loadCompiler} from '../utils/build.js'; +import { + exposeEnvs, + loadCompiler, + extractCompileOptions, + parseCompileFileContext, +} from '../utils/build.js'; import {createCLICommand} from '../utils/cli.js'; export const compileCommand = createCLICommand( @@ -21,13 +27,25 @@ export const compileCommand = createCLICommand( type: 'boolean', description: 'Also watch for changes.', }, + target: { + alias: 't', + type: 'string', + description: 'Target: development (default), production, ...', + }, }, }, async (args, callbacks) => { const tiniProject = await getTiniProject(); - const {config: tiniConfig} = tiniProject; - const {srcDir, compileDir} = tiniConfig; + const {config: tiniConfig, hooks} = tiniProject; + const projectDirs = getProjectDirs(tiniConfig); + const {srcDir, compileDir} = projectDirs; + const compileOptions = extractCompileOptions<{ + ignorePatterns?: string[]; + }>(tiniConfig.compile); + // preparation + exposeEnvs(tiniConfig, args.target || 'development'); const compiler = await loadCompiler(tiniProject, true); + // compile if (!compiler) { callbacks?.onUnavailable(); } else { @@ -35,8 +53,7 @@ export const compileCommand = createCLICommand( if (!args.watch) { callbacks?.onSingleCompile(compileDir); } else { - const srcDirPath = resolve(srcDir); - watch(srcDirPath, {ignoreInitial: true}) + watch(resolve(srcDir), {ignoreInitial: true}) .on('add', path => { callbacks?.onCompileFile('add', path); compiler.compileFile(resolve(path)); @@ -45,11 +62,20 @@ export const compileCommand = createCLICommand( callbacks?.onCompileFile('change', path); compiler.compileFile(resolve(path)); }) - .on('unlink', path => { + .on('unlink', async path => { callbacks?.onCompileFile('unlink', path); - remove( - resolve(compileDir, resolve(path).replace(`${srcDirPath}/`, '')) + const ignoreMatcher = !compileOptions.ignorePatterns + ? undefined + : picomatch(compileOptions.ignorePatterns); + const context = await parseCompileFileContext( + path, + projectDirs, + ignoreMatcher ); + if (!context) return; + await hooks.callHook('compile:beforeRemoveFile', context); + await remove(context.outPath); + await hooks.callHook('compile:afterRemoveFile', context); }); callbacks?.onCompileAndWatch(compileDir); } diff --git a/packages/cli/cli/commands/dev.ts b/packages/cli/cli/commands/dev.ts index cf38a3a0..fd231bc4 100644 --- a/packages/cli/cli/commands/dev.ts +++ b/packages/cli/cli/commands/dev.ts @@ -16,11 +16,10 @@ export const devCommand = createCLICommand( }, }, async (args, callbacks) => { - const targetEnv = 'development'; const tiniProject = await getTiniProject(); const {config: tiniConfig, hooks} = tiniProject; // preparation - exposeEnvs(tiniConfig, targetEnv); + exposeEnvs(tiniConfig, 'development'); const builder = await loadBuilder(tiniProject); // start dev await hooks.callHook('dev:before'); diff --git a/packages/cli/cli/utils/build.ts b/packages/cli/cli/utils/build.ts index 6268d7e6..6e032f41 100644 --- a/packages/cli/cli/utils/build.ts +++ b/packages/cli/cli/utils/build.ts @@ -1,9 +1,15 @@ +import {resolve, parse} from 'pathe'; +import {readFile} from 'node:fs/promises'; +import type {Matcher} from 'picomatch'; import { TiniProject, getProjectDirs, + isUnderTopDir, type TiniConfig, type Compiler, type Builder, + type ProjectDirs, + type CompileFileHookContext, } from '@tinijs/project'; export async function loadCompiler( @@ -37,9 +43,7 @@ export function exposeEnvs(tiniConfig: TiniConfig, targetEnv: string) { process.env.TARGET_ENV = targetEnv; process.env.TINI_PROJECT_DIRS = JSON.stringify(projectDirs); process.env.TINI_COMPILE_OPTIONS = JSON.stringify( - (tiniConfig.compile === false || tiniConfig.compile instanceof Function - ? undefined - : tiniConfig.compile?.options) || {} + extractCompileOptions(tiniConfig.compile) ); process.env.TINI_BUILD_OPTIONS = JSON.stringify( (tiniConfig.build instanceof Function @@ -47,3 +51,51 @@ export function exposeEnvs(tiniConfig: TiniConfig, targetEnv: string) { : tiniConfig.build?.options) || {} ); } + +export function extractCompileOptions( + compileConfig: TiniConfig['compile'] +) { + return ((compileConfig === false || compileConfig instanceof Function + ? undefined + : compileConfig?.options) || {}) as Type; +} + +export async function parseCompileFileContext( + path: string, + {srcDir, compileDir, dirs}: ProjectDirs, + ignoreMatcher?: Matcher +): Promise { + if (ignoreMatcher?.(path)) return null; + const env = process.env.TARGET_ENV || 'development'; + const isDevelopment = env === 'development'; + const inPath = resolve(path); + const outPath = resolve( + compileDir, + inPath.replace(`${resolve(srcDir)}/`, '') + ); + const {base, name, ext} = parse(inPath); + const isPublic = isUnderTopDir(inPath, srcDir, dirs.public); + const copyOnly = !!( + isPublic || !['.html', '.css', '.scss', '.ts', '.js'].includes(ext) + ); + const isAppEntry = path.endsWith(`${srcDir}/index.html`); + const isAppRoot = path.endsWith(`${srcDir}/app.ts`); + const isActiveConfig = path.endsWith( + `${srcDir}/${dirs.configs}/${process.env.TARGET_ENV}.ts` + ); + return { + env, + isDevelopment, + base, + name, + ext, + inPath, + outPath, + content: copyOnly ? undefined : await readFile(inPath, 'utf8'), + copyOnly, + isPublic, + isAppEntry, + isAppRoot, + isActiveConfig, + }; +} diff --git a/packages/cli/package.json b/packages/cli/package.json index 4de685a5..be5f74bf 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -60,7 +60,7 @@ "open": "^10.0.4", "ora": "^8.0.1", "pathe": "^1.1.2", - "picomatch": "^4.0.1", + "picomatch": "^4.0.2", "prettier": "^3.2.5", "recursive-readdir": "^2.2.3", "superstatic": "^9.0.3", diff --git a/packages/default-compiler/lib/index.ts b/packages/default-compiler/lib/index.ts index 1c684540..d7dbb5a4 100644 --- a/packages/default-compiler/lib/index.ts +++ b/packages/default-compiler/lib/index.ts @@ -1,17 +1,29 @@ -import {resolve, parse} from 'pathe'; -import {readFile} from 'node:fs/promises'; +import {resolve} from 'pathe'; import {outputFile, copy} from 'fs-extra/esm'; +import picomatch from 'picomatch'; +import {minifyHTMLLiterals} from 'minify-html-literals'; +import {compileStringAsync, type StringOptions} from 'sass'; import { TiniProject, getProjectDirs, type Compiler, type CompileFileHookContext, + type CommonCompileOptions, } from '@tinijs/project'; -import {listDir, cleanDir} from '@tinijs/cli'; +import { + listDir, + cleanDir, + extractCompileOptions, + parseCompileFileContext, +} from '@tinijs/cli'; -export interface CompileOptions { - skipMinifyHTMLLiterals?: boolean; - precompileGeneric?: 'none' | 'lite' | 'full'; +export interface CompileOptions extends CommonCompileOptions { + compileTaggedHTML?: + | false + | { + minify?: false; + }; + compileTaggedCSS?: false | Omit, 'style'>; } export default function (options: CompileOptions, tiniProject: TiniProject) { @@ -24,55 +36,153 @@ export class DefaultCompiler implements Compiler { private tiniProject: TiniProject ) {} + private get projectDirs() { + return getProjectDirs(this.tiniProject.config); + } + async compile() { - const srcPath = resolve(this.tiniProject.config.srcDir); - await cleanDir(this.tiniProject.config.compileDir); - const paths = await listDir(srcPath); - await this.tiniProject.hooks.callHook('compile:before'); + const {srcDir, compileDir} = this.projectDirs; + const {hooks} = this.tiniProject; + await cleanDir(compileDir); + const paths = await listDir(resolve(srcDir)); + await hooks.callHook('compile:before'); for (let i = 0; i < paths.length; i++) { await this.compileFile(paths[i]); } - await this.tiniProject.hooks.callHook('compile:after'); + await hooks.callHook('compile:after'); } - async compileFile(inPath: string) { - const {srcDir, dirs} = getProjectDirs(this.tiniProject.config); - const {base, ext} = parse(inPath); - const outPath = resolve( - this.tiniProject.config.compileDir, - inPath.replace(`${resolve(this.tiniProject.config.srcDir)}/`, '') + async compileFile(path: string) { + const {config: tiniConfig, hooks} = this.tiniProject; + const compileOptions = extractCompileOptions( + tiniConfig.compile ); - const context: CompileFileHookContext | null = - this.isUnderTopDir(inPath, srcDir, dirs.public) || - !['.html', '.css', '.scss', '.ts', '.js'].includes(ext) - ? {base, inPath, outPath, content: ''} - : { - base, - inPath, - outPath, - content: await readFile(inPath, 'utf8'), - }; + const ignoreMatcher = !compileOptions.ignorePatterns + ? undefined + : picomatch(compileOptions.ignorePatterns); + const context = await parseCompileFileContext( + path, + this.projectDirs, + ignoreMatcher + ); + if (!context) return; + // builtin compile file + await hooks.callHook('compile:beforeCompileFile', context); + await this.builtinFileCompile(context); + await hooks.callHook('compile:afterCompileFile', context); + // save file + await hooks.callHook('compile:beforeOutputFile', context); + if (!context.content) { + await copy(context.inPath, context.outPath); + } else { + await outputFile(context.outPath, context.content); + } + await hooks.callHook('compile:afterOutputFile', context); + } - if (context) { - // build file - await this.tiniProject.hooks.callHook('compile:beforeFile', context); - await this.builtinFileBuilder(context); - await this.tiniProject.hooks.callHook('compile:afterFile', context); + private async builtinFileCompile(context: CompileFileHookContext) { + if (!context.content) return; + const {compileTaggedHTML, compileTaggedCSS} = this.options; + const {dirs} = this.projectDirs; - // save file - if (!context.content) { - copy(context.inPath, context.outPath); - } else { - outputFile(context.outPath, context.content); - } + // inject config envs & replace config imports + if (context.isActiveConfig) { + this.injectConfigEnvs(context); + } + if (!context.isDevelopment) { + context.content = context.content.replace( + new RegExp(`./${dirs.configs}/development.js';`, 'g'), + `./${dirs.configs}/${context.env}.js';` + ); + } + + // .ts, .js + if (['.ts', '.js'].includes(context.ext)) { + if (compileTaggedHTML !== false) + await this.handleTaggedHTML(context, compileTaggedHTML); + if (compileTaggedCSS !== false) + await this.handleTaggedCSS(context, compileTaggedCSS); } } - private async builtinFileBuilder(context: CompileFileHookContext) { - // eslint-disable-next-line no-empty + private injectConfigEnvs(context: CompileFileHookContext) { + const envMatchingArr = context.content!.match( + /process\.env\.([\s\S]*?)(,|\n)/g + ); + if (!envMatchingArr?.length) return; + envMatchingArr.forEach(envMatching => { + const replaceStr = envMatching.replace(/,|\n/g, ''); + context.content = context.content!.replace( + replaceStr, + `"${process.env[replaceStr.replace('process.env.', '')]}"` + ); + }); } - private isUnderTopDir(path: string, srcDir: string, topDir: string) { - return ~path.indexOf(`/${srcDir}/${topDir}/`); + private async handleTaggedHTML( + context: CompileFileHookContext, + options: Exclude + ) { + const templateMatching = context.content!.match( + /(return html`)([\s\S]*?)(`;)/ + ); + if (!templateMatching) return; // no return html`...` + const {isDevelopment} = context; + // minify template literals + if (!isDevelopment) { + const matchedTemplate = templateMatching[0]; + let minifiedTemplate: string | undefined; + // minify + try { + if (options?.minify !== false) { + const result = minifyHTMLLiterals(matchedTemplate); + if (!result) throw new Error('Run minifyHTMLLiterals() failed.'); + minifiedTemplate = result.code; + } + } catch (error) { + // eslint-disable-next-line no-empty + } + // replace original with minified + if (minifiedTemplate) { + context.content = context.content!.replace( + matchedTemplate, + minifiedTemplate + ); + } + } + } + + private async handleTaggedCSS( + context: CompileFileHookContext, + options: Exclude + ) { + const cssMatchingArr = context.content!.match(/(css`)([\s\S]*?)(`,|`;)/g); + if (!cssMatchingArr?.length) return; // no css`` + const {isDevelopment} = context; + // compile scss + for (let i = 0; i < cssMatchingArr.length; i++) { + const cssMatching = cssMatchingArr[i]; + if (cssMatching.includes('/* no-sass */')) continue; + // extract original + let originalValue = cssMatching.replace('css`', ''); + originalValue = originalValue.substring(0, originalValue.length - 2); + // compile scss + let compiledValue: string; + try { + compiledValue = ( + await compileStringAsync(originalValue, { + loadPaths: ['node_modules', this.projectDirs.srcDir], + ...options, + style: isDevelopment ? 'expanded' : 'compressed', + }) + ).css; + } catch (err) { + compiledValue = isDevelopment + ? originalValue + : originalValue.replace(/(?:\r\n|\r|\n)/g, '').replace(/\s\s+/g, ' '); + } + // replacing original with compiled + context.content = context.content!.replace(originalValue, compiledValue); + } } } diff --git a/packages/default-compiler/package.json b/packages/default-compiler/package.json index a2844100..7ffb90e1 100644 --- a/packages/default-compiler/package.json +++ b/packages/default-compiler/package.json @@ -37,7 +37,9 @@ "@tinijs/cli": "^0.17.0", "@tinijs/project": "^0.17.0", "fs-extra": "^11.2.0", + "minify-html-literals": "^1.3.5", "pathe": "^1.1.2", - "sass": "^1.71.1" + "picomatch": "^4.0.2", + "sass": "^1.75.0" } } diff --git a/packages/project/lib/types/build.ts b/packages/project/lib/types/build.ts index 1dc7c3fd..bdaf1bde 100644 --- a/packages/project/lib/types/build.ts +++ b/packages/project/lib/types/build.ts @@ -14,7 +14,7 @@ export interface CommonBuildOptions { } export interface Builder { - options: Record; + options: NonNullable; dev: | (() => Promise) | { diff --git a/packages/project/lib/types/compile.ts b/packages/project/lib/types/compile.ts index 65a8ed52..51a1f191 100644 --- a/packages/project/lib/types/compile.ts +++ b/packages/project/lib/types/compile.ts @@ -2,8 +2,12 @@ import type {HookCallback} from 'hookable'; import type {TiniProject} from '../classes/project.js'; +export interface CommonCompileOptions { + ignorePatterns?: string[]; +} + export interface Compiler { - options: Record; + options: NonNullable; compile: () => Promise; compileFile: (path: string) => Promise; } @@ -17,18 +21,39 @@ export type CustomCompileConfig = (tiniProject: TiniProject) => Compiler; export interface CompileHooks { 'compile:before': () => ReturnType; - 'compile:beforeFile': ( + 'compile:after': () => ReturnType; + 'compile:beforeCompileFile': ( context: CompileFileHookContext ) => ReturnType; - 'compile:afterFile': ( + 'compile:afterCompileFile': ( + context: CompileFileHookContext + ) => ReturnType; + 'compile:beforeOutputFile': ( + context: CompileFileHookContext + ) => ReturnType; + 'compile:afterOutputFile': ( + context: CompileFileHookContext + ) => ReturnType; + 'compile:beforeRemoveFile': ( + context: CompileFileHookContext + ) => ReturnType; + 'compile:afterRemoveFile': ( context: CompileFileHookContext ) => ReturnType; - 'compile:after': () => ReturnType; } export interface CompileFileHookContext { + env: string; + isDevelopment: boolean; base: string; + name: string; + ext: string; inPath: string; outPath: string; - content: string; + content?: string; + copyOnly: boolean; + isPublic: boolean; + isAppEntry: boolean; + isAppRoot: boolean; + isActiveConfig: boolean; } diff --git a/packages/project/lib/utils/dir.ts b/packages/project/lib/utils/dir.ts index 7cdfd69a..c0229fc2 100644 --- a/packages/project/lib/utils/dir.ts +++ b/packages/project/lib/utils/dir.ts @@ -1,5 +1,7 @@ import type {TiniConfig} from '../classes/project.js'; +export type ProjectDirs = ReturnType; + export const DEFAULT_SRC_DIR = 'app'; export const DEFAULT_COMPILE_DIR = '.app'; export const DEFAULT_OUT_DIR = '.output'; @@ -35,3 +37,7 @@ export function getProjectDirs({ }, }; } + +export function isUnderTopDir(path: string, srcDir: string, topDir: string) { + return path.includes(`/${srcDir}/${topDir}/`); +} From 42132eedd5673f1ed532903bd803dac8c825aade Mon Sep 17 00:00:00 2001 From: Nhan Lam Date: Mon, 29 Apr 2024 12:04:17 +0700 Subject: [PATCH 3/3] Add compile docs --- apps/tinijs.dev/app/pages/module.ts | 18 +++ .../framework-posts/104 - compile/index.md | 114 +++++++++++++++++- packages/default-compiler/lib/index.ts | 62 ++++------ 3 files changed, 152 insertions(+), 42 deletions(-) diff --git a/apps/tinijs.dev/app/pages/module.ts b/apps/tinijs.dev/app/pages/module.ts index 20dc7518..3deb7d8f 100644 --- a/apps/tinijs.dev/app/pages/module.ts +++ b/apps/tinijs.dev/app/pages/module.ts @@ -32,6 +32,24 @@ export class AppPageModule extends TiniComponent { return html`

Tini Modules

Installable modules to extend functionalities.

+ +

Official modules

+
    +
  • + Content - file-based content management + system +
  • +
  • PWA - turn a TiniJS app into a PWA
  • +
+ +

Community/local modules

+
    +
  • Add your shared module here
  • +
+

+ Please see the Author Guide for how + to create a module. +

`; } diff --git a/apps/tinijs.dev/content/framework-posts/104 - compile/index.md b/apps/tinijs.dev/content/framework-posts/104 - compile/index.md index a88fd0ce..8b3263f5 100644 --- a/apps/tinijs.dev/content/framework-posts/104 - compile/index.md +++ b/apps/tinijs.dev/content/framework-posts/104 - compile/index.md @@ -6,4 +6,116 @@ } +++ -**TODO**: implements Default Compiler features and add instructions. +Compile is an optional but recommended step in the dev/build workflow for TiniJS apps. You can think of it as a transformation step that takes the source code and transforms it before forwarding the code to the build tool. + +With the **Default Compiler**, you can use these features: +- Support `SCSS` in `` css`...` `` +- Minify the content `` html`...` `` +- Inject environment variables to `configs/.ts` and choose a config file based on the target environment +- _More in the future ..._ + +You can config the compile step behaviors in the `tini.config.ts` file. + +```js +export default defineTiniConfig({ + + // compile step is enabled by default + compile: { + + // default to the Default Compiler (@tinijs/default-compiler) + // you can use your own compiler + compiler: 'a-compiler', + + // you can set options for the Default Compiler + options: { + // ignore certain files, please see: https://github.com/micromatch/picomatch + ignorePatterns?: string[], + // compile html``, default is enable, set false to disable + compileTaggedHTML?: false | { + minify?: false; // minify the template literals or not (default is minified) + }, + // compile css``, default is enable, set false to disable + // or set options for SASS: https://sass-lang.com/documentation/js-api/interfaces/stringoptions/ + compileTaggedCSS?: false | SASSOptions, + } + }, + + // or, disable the compile step + compile: false, + +}); +``` + +## Compile HTML + +Look for `` return html` `` and `` `; `` in your source code, the content inside the template literals will be compiled. + +**Limitations**, nested `` html`...` `` will be kept as is, split the nested templates into smaller templates instead: + +```ts + +class XXX { + + helloTemplate() { + return html`

Hello World

`; + } + + render() { + return html` +
${this.helloTemplate()}
+ `; + } +} +``` + +## Compile CSS + +Look for `` css` `` and `` `; ``/`` `, `` in your source code, the content inside the template literals will be compiled. + +**Limitations**, use `${}` inside the templates will break SASS compilation, add `/* no-sass */` to skip. + +```ts +const style1 = css` + // scss code + // will be compiled +`; + +const style2 = ` + // scss code + // will not be compiled +`; + +class XXX { + + static styles = css` + /* no-sass */ + + // scss code + // will not be compiled + + ${style1} + ${unsafeCSS(style2)} + `; + +} +``` + +## Client app configs + +Put `configs/.ts` files in the `app` folder, the environment variables will be injected into the files. + +**IMPORTANT**: all the config in `configs` folder are for the client app only, don't expose sensitive information. + +```ts +export const config: AppConfig = { + foo: 'bar', + baz: process.env.BAZ, + qux: process.env.QUX, +}; +``` + +During the `npm run dev` the file `configs/development.ts` will be used, during the `npm run build` the file `configs/production.ts` or `configs/.ts` (flag `--target `) will be used. + +```bash +npx tini build --target qa|staging|xxx +``` diff --git a/packages/default-compiler/lib/index.ts b/packages/default-compiler/lib/index.ts index d7dbb5a4..34d4f488 100644 --- a/packages/default-compiler/lib/index.ts +++ b/packages/default-compiler/lib/index.ts @@ -123,31 +123,20 @@ export class DefaultCompiler implements Compiler { context: CompileFileHookContext, options: Exclude ) { - const templateMatching = context.content!.match( - /(return html`)([\s\S]*?)(`;)/ + const templateMatchingArr = context.content!.match( + /(return html`)([\s\S]*?)(`;)/g ); - if (!templateMatching) return; // no return html`...` - const {isDevelopment} = context; + if (!templateMatchingArr) return; // no return html`...` // minify template literals - if (!isDevelopment) { - const matchedTemplate = templateMatching[0]; - let minifiedTemplate: string | undefined; - // minify - try { - if (options?.minify !== false) { - const result = minifyHTMLLiterals(matchedTemplate); - if (!result) throw new Error('Run minifyHTMLLiterals() failed.'); - minifiedTemplate = result.code; + if (options?.minify !== false) { + for (const matchedTemplate of templateMatchingArr) { + const result = minifyHTMLLiterals(matchedTemplate); + if (result) { + context.content = context.content!.replace( + matchedTemplate, + result.code + ); } - } catch (error) { - // eslint-disable-next-line no-empty - } - // replace original with minified - if (minifiedTemplate) { - context.content = context.content!.replace( - matchedTemplate, - minifiedTemplate - ); } } } @@ -158,29 +147,20 @@ export class DefaultCompiler implements Compiler { ) { const cssMatchingArr = context.content!.match(/(css`)([\s\S]*?)(`,|`;)/g); if (!cssMatchingArr?.length) return; // no css`` - const {isDevelopment} = context; // compile scss - for (let i = 0; i < cssMatchingArr.length; i++) { - const cssMatching = cssMatchingArr[i]; - if (cssMatching.includes('/* no-sass */')) continue; + for (const matchedCSS of cssMatchingArr) { + if (matchedCSS.includes('/* no-sass */')) continue; // extract original - let originalValue = cssMatching.replace('css`', ''); + let originalValue = matchedCSS.replace('css`', ''); originalValue = originalValue.substring(0, originalValue.length - 2); // compile scss - let compiledValue: string; - try { - compiledValue = ( - await compileStringAsync(originalValue, { - loadPaths: ['node_modules', this.projectDirs.srcDir], - ...options, - style: isDevelopment ? 'expanded' : 'compressed', - }) - ).css; - } catch (err) { - compiledValue = isDevelopment - ? originalValue - : originalValue.replace(/(?:\r\n|\r|\n)/g, '').replace(/\s\s+/g, ' '); - } + const compiledValue = ( + await compileStringAsync(originalValue, { + loadPaths: ['node_modules', this.projectDirs.srcDir], + ...options, + style: 'compressed', + }) + ).css; // replacing original with compiled context.content = context.content!.replace(originalValue, compiledValue); }