├── .npmignore ├── packages ├── dot-template-core │ ├── src │ │ ├── test │ │ │ └── core │ │ │ │ ├── fixtures │ │ │ │ ├── related │ │ │ │ │ ├── style │ │ │ │ │ ├── inject │ │ │ │ │ ├── no-inject │ │ │ │ │ └── dtpl.js │ │ │ │ ├── .dtpl │ │ │ │ │ ├── folder │ │ │ │ │ │ └── a.txt │ │ │ │ │ ├── upper.dtpl │ │ │ │ │ ├── variable.dtpl │ │ │ │ │ ├── text.txt │ │ │ │ │ └── dtpl.js │ │ │ │ ├── render │ │ │ │ │ ├── r1.dtpl │ │ │ │ │ ├── r4.txt │ │ │ │ │ ├── r2.ejs │ │ │ │ │ ├── r3.njk │ │ │ │ │ └── dtpl.js │ │ │ │ └── js │ │ │ │ │ ├── dir │ │ │ │ │ ├── text.txt │ │ │ │ │ ├── ${MODULE_NAME}.js │ │ │ │ │ └── foo │ │ │ │ │ │ └── readme.md.dtpl │ │ │ │ │ └── dtpl.js │ │ │ │ ├── inc │ │ │ │ ├── index.ts │ │ │ │ ├── helper.ts │ │ │ │ ├── File.ts │ │ │ │ └── Project.ts │ │ │ │ ├── test_render.ts │ │ │ │ ├── test_calculateStartInjectPoint.ts │ │ │ │ ├── test_undoTimeout.ts │ │ │ │ ├── test_common.ts │ │ │ │ ├── test_emitNewFile.ts │ │ │ │ ├── test_createDirectories.ts │ │ │ │ └── test_createRelated.ts │ │ ├── core │ │ │ ├── file │ │ │ │ ├── index.ts │ │ │ │ ├── Template.ts │ │ │ │ └── Source.ts │ │ │ ├── commands │ │ │ │ ├── index.ts │ │ │ │ ├── CreateTemplateFilesCommand.ts │ │ │ │ ├── CreateDirectoriesCommand.ts │ │ │ │ ├── Command.ts │ │ │ │ └── CreateRelatedFilesCommand.ts │ │ │ ├── common.ts │ │ │ ├── Render.ts │ │ │ ├── Editor.ts │ │ │ ├── Commander.ts │ │ │ └── Application.ts │ │ ├── index.ts │ │ ├── common │ │ │ ├── transformer.ts │ │ │ ├── data.ts │ │ │ └── helper.ts │ │ ├── install-types.ts │ │ ├── adapter │ │ │ └── TestEditor.ts │ │ └── config │ │ │ ├── dtpl.ts │ │ │ └── config.json │ ├── res │ │ ├── template │ │ │ ├── template │ │ │ │ ├── page.css.dtpl │ │ │ │ ├── example │ │ │ │ │ ├── page │ │ │ │ │ │ ├── style │ │ │ │ │ │ │ └── Home.css │ │ │ │ │ │ ├── About.tsx.dtpl │ │ │ │ │ │ └── Home.tsx.dtpl │ │ │ │ │ ├── config │ │ │ │ │ │ ├── .gitignore.dtpl │ │ │ │ │ │ ├── tsconfig.json.dtpl │ │ │ │ │ │ ├── package.json │ │ │ │ │ │ └── webpack.config.js.dtpl │ │ │ │ │ ├── widget │ │ │ │ │ │ └── Footer.tsx.dtpl │ │ │ │ │ ├── index.html.dtpl │ │ │ │ │ ├── index.css │ │ │ │ │ └── index.tsx.dtpl │ │ │ │ ├── page.tsx.dtpl │ │ │ │ └── widget.tsx.dtpl │ │ │ ├── top-no-example │ │ │ │ └── dtpl.cjs.ejs │ │ │ └── top-example │ │ │ │ ├── dtpl.js.ejs │ │ │ │ ├── readme.md │ │ │ │ └── dtpl.ts.ejs │ │ └── doc-images │ │ │ ├── uml.png │ │ │ └── doc.sketch │ ├── .vscode │ │ └── settings.json │ ├── README.md │ ├── .npmignore │ ├── tsconfig.json │ └── package.json ├── dot-template-types │ ├── .npmignore │ ├── index.d.ts │ ├── core │ │ ├── file │ │ │ ├── index.d.ts │ │ │ ├── Template.d.ts │ │ │ └── Source.d.ts │ │ ├── commands │ │ │ ├── index.d.ts │ │ │ ├── CreateDirectoriesCommand.d.ts │ │ │ ├── CreateRelatedFilesCommand.d.ts │ │ │ ├── CreateTemplateFilesCommand.d.ts │ │ │ └── Command.d.ts │ │ ├── common.d.ts │ │ ├── Render.d.ts │ │ ├── Commander.d.ts │ │ ├── Application.d.ts │ │ └── Editor.d.ts │ ├── README.md │ ├── package.json │ └── common │ │ ├── transformer.d.ts │ │ ├── helper.d.ts │ │ └── data.d.ts ├── dot-template-cli │ ├── src │ │ ├── app │ │ │ ├── index.ts │ │ │ ├── Client.ts │ │ │ ├── common.ts │ │ │ └── Watcher.ts │ │ ├── adapter │ │ │ └── CliEditor.ts │ │ └── cli.ts │ ├── .npmignore │ ├── README.md │ ├── tsconfig.json │ └── package.json └── dot-template-vscode │ ├── src │ ├── app │ │ ├── index.ts │ │ ├── HoverProvider.ts │ │ ├── AutoCompletion.ts │ │ ├── AppRelied.ts │ │ └── App.ts │ ├── test │ │ ├── extension.test.ts │ │ └── index.ts │ ├── vscode.ts │ └── adapter │ │ └── VscodeEditor.ts │ ├── res │ ├── images │ │ └── icon.png │ ├── snippets │ │ ├── inject-hash.json │ │ ├── inject-html.json │ │ └── inject-docs.json │ ├── language-configuration.json │ └── syntaxes │ │ └── dtpl.tmLanguage.json │ ├── .npmignore │ ├── .vscodeignore │ ├── tsconfig.json │ ├── vsc-extension-quickstart.md │ └── package.json ├── .gitignore ├── lerna.json ├── CHANGELOG.md ├── .travis.yml ├── .gitmessage ├── appveyor.yml ├── dot-template.code-workspace ├── .vscode └── launch.json ├── tsconfig.json ├── package.json ├── TODO.md ├── README.md └── src └── bin.ts /.npmignore: -------------------------------------------------------------------------------- 1 | * 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/related/style: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/.dtpl/folder/a.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/dot-template-types/.npmignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | dtpl.*.log 3 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/related/inject: -------------------------------------------------------------------------------- 1 | template 2 | -------------------------------------------------------------------------------- /packages/dot-template-types/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './common/interface' 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/render/r1.dtpl: -------------------------------------------------------------------------------- 1 | ${MODULE_NAME} 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/render/r4.txt: -------------------------------------------------------------------------------- 1 | MODULE_NAME 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/.dtpl/upper.dtpl: -------------------------------------------------------------------------------- 1 | ${MODULE_NAME} 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/.dtpl/variable.dtpl: -------------------------------------------------------------------------------- 1 | ${foo.bar} 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/js/dir/text.txt: -------------------------------------------------------------------------------- 1 | ${MODULE_NAME} 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/render/r2.ejs: -------------------------------------------------------------------------------- 1 | <%=MODULE_NAME%> 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/render/r3.njk: -------------------------------------------------------------------------------- 1 | {{ MODULE_NAME }} 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/js/dir/${MODULE_NAME}.js: -------------------------------------------------------------------------------- 1 | ${fileName} 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/related/no-inject: -------------------------------------------------------------------------------- 1 | no-inject content 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/page.css.dtpl: -------------------------------------------------------------------------------- 1 | .p${ModuleName} { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/.dtpl/text.txt: -------------------------------------------------------------------------------- 1 | Just text no replace $fileName 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/file/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Source' 2 | export * from './Template' 3 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/file/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './Source'; 2 | export * from './Template'; 3 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/js/dir/foo/readme.md.dtpl: -------------------------------------------------------------------------------- 1 | ${MODULE_NAME} ref:${ref.MODULE_NAME} 2 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/page/style/Home.css: -------------------------------------------------------------------------------- 1 | .pHome { 2 | color: chocolate; 3 | } 4 | -------------------------------------------------------------------------------- /packages/dot-template-cli/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | export * from './Watcher' 3 | export * from './Client' 4 | -------------------------------------------------------------------------------- /packages/dot-template-types/README.md: -------------------------------------------------------------------------------- 1 | # dot-template-types 2 | 3 | 从 `dot-template-core` 中提取出来的 typings 文件,方便在配置文件 `dtpl.ts` 中引用 4 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/config/.gitignore.dtpl: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dtpl.error.log 3 | dtpl.debug.log 4 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/inc/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helper' 2 | export * from './Project' 3 | export * from './File' 4 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from './App' 2 | export * from './AutoCompletion' 3 | export * from './HoverProvider' -------------------------------------------------------------------------------- /packages/dot-template-core/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './core/common' 2 | export * from './core/Editor' 3 | export * from './core/Application' 4 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/res/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiu8310/dot-template/HEAD/packages/dot-template-vscode/res/images/icon.png -------------------------------------------------------------------------------- /packages/dot-template-core/res/doc-images/uml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiu8310/dot-template/HEAD/packages/dot-template-core/res/doc-images/uml.png -------------------------------------------------------------------------------- /packages/dot-template-core/res/doc-images/doc.sketch: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiu8310/dot-template/HEAD/packages/dot-template-core/res/doc-images/doc.sketch -------------------------------------------------------------------------------- /packages/dot-template-cli/.npmignore: -------------------------------------------------------------------------------- 1 | out/test/** 2 | out/**/*.map 3 | src/** 4 | tsconfig.json 5 | TODO.md 6 | yarn-* 7 | 8 | .DS_Store 9 | .nyc_output/ 10 | dtpl.*.log 11 | -------------------------------------------------------------------------------- /packages/dot-template-core/README.md: -------------------------------------------------------------------------------- 1 | # dot-template-core 2 | 3 | dot-template 的核心代码,`dot-template-cli` 和 `dot-template-vscode` 都依赖于它, 4 | 如果需要创建其它编辑器的 dot-template 的插件,可以使用此组件 5 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CreateDirectoriesCommand' 2 | export * from './CreateRelatedFilesCommand' 3 | export * from './CreateTemplateFilesCommand' 4 | -------------------------------------------------------------------------------- /packages/dot-template-core/.npmignore: -------------------------------------------------------------------------------- 1 | out/test/** 2 | out/**/*.map 3 | src/** 4 | res/doc-images/ 5 | tsconfig.json 6 | TODO.md 7 | yarn-* 8 | coverage/** 9 | .nyc_output/** 10 | .DS_Store 11 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/common.ts: -------------------------------------------------------------------------------- 1 | export * from '../common/helper' 2 | export * from '../common/interface' 3 | export * from '../common/data' 4 | export * from '../common/transformer' 5 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/commands/index.d.ts: -------------------------------------------------------------------------------- 1 | export * from './CreateDirectoriesCommand'; 2 | export * from './CreateRelatedFilesCommand'; 3 | export * from './CreateTemplateFilesCommand'; 4 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/common.d.ts: -------------------------------------------------------------------------------- 1 | export * from '../common/helper'; 2 | export * from '../common/interface'; 3 | export * from '../common/data'; 4 | export * from '../common/transformer'; 5 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/res/snippets/inject-hash.json: -------------------------------------------------------------------------------- 1 | { 2 | "inject": { 3 | "prefix": "inject", 4 | "body": "## INJECT_START {\"key\": \"${1:key}\"} ##\n## INJECT_END ##", 5 | "description": "the tags of using mora-script to inject content" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/.npmignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | .vscodeignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | vsc-commands.md 10 | TODO.md 11 | coverage/** 12 | .nyc_output/** 13 | .DS_Store 14 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/res/language-configuration.json: -------------------------------------------------------------------------------- 1 | { 2 | // symbols used as brackets 3 | "brackets": [ 4 | ["${", "}"] 5 | ], 6 | // symbols that are auto closed when typing 7 | "autoClosingPairs": [ 8 | ["${", "}"] 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/res/snippets/inject-html.json: -------------------------------------------------------------------------------- 1 | { 2 | "inject": { 3 | "prefix": "inject", 4 | "body": "\n", 5 | "description": "the tags of using mora-script to inject content" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | out/**/*.map 5 | src/** 6 | .npmignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | vsc-commands.md 10 | TODO.md 11 | yarn.lock 12 | coverage/** 13 | .nyc_output/** 14 | .DS_Store 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | packages/*/out 3 | packages/*/node_modules 4 | packages/dot-template-vscode/README.md 5 | packages/dot-template-vscode/CHANGELOG.md 6 | packages/dot-template-vscode/ARTICLE_ABOUT_IT.md 7 | node_modules/ 8 | .vscode-test 9 | 10 | package-lock.json 11 | .nyc_output/ 12 | .DS_Store 13 | dtpl.*.log 14 | yarn-*.log 15 | lerna-*.log 16 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "lerna": "2.11.0", 3 | "npmClient": "npm", 4 | "registry": "https://registry.npmjs.org/", 5 | "packages": [ 6 | "packages/*" 7 | ], 8 | "commands": { 9 | "publish": { 10 | "ignore": [ 11 | "**/CHANGELOG.md", 12 | "**/README.md", 13 | "**/tsconfig.json" 14 | ] 15 | } 16 | }, 17 | "version": "0.3.0" 18 | } 19 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/page.tsx.dtpl: -------------------------------------------------------------------------------- 1 | /** 2 | * Create by $user at $datetime 3 | * All right reserved 4 | */ 5 | 6 | import * as React from 'react' 7 | 8 | export class ${ModuleName} extends React.Component { 9 | render() { 10 | return ( 11 |
12 | This is ${ModuleName} page 13 |
14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/widget.tsx.dtpl: -------------------------------------------------------------------------------- 1 | /** 2 | * Create by $user at $datetime 3 | * All right reserved 4 | */ 5 | 6 | import * as React from 'react' 7 | 8 | export class ${ModuleName} extends React.Component { 9 | render() { 10 | return ( 11 |
12 | This is ${ModuleName} widget 13 |
14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/dot-template-types/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dot-template-types", 3 | "description": "create new files according to pre-defined templates", 4 | "version": "0.3.0", 5 | "typings": "index.d.ts", 6 | "keywords": [ 7 | "template", 8 | "file", 9 | "variable" 10 | ], 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/qiu8310/dot-template" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | v0.0.2 / 2018-2-7 2 | =================== 3 | 4 | - 修证文档中的错误 5 | 6 | v0.0.1 / 2018-2-7 7 | =================== 8 | 9 | - Initial release 10 | 11 | 12 | Change Log 13 | =================== 14 | All notable changes to the "dot-template-example" extension will be documented in this file. 15 | 16 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 17 | 18 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/page/About.tsx.dtpl: -------------------------------------------------------------------------------- 1 | /** 2 | * Create by $user at $datetime 3 | * All right reserved 4 | */ 5 | 6 | import * as React from 'react' 7 | 8 | export class ${ModuleName} extends React.Component { 9 | render() { 10 | return ( 11 |
12 | This is ${ModuleName} page 13 |
14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | env: CI=true 3 | sudo: false 4 | 5 | install: 6 | - npm -g install lerna 7 | - npm install 8 | - lerna bootstrap 9 | 10 | # before_script: npm run build 11 | 12 | script: npm test 13 | 14 | os: 15 | - linux 16 | - osx 17 | 18 | node_js: 19 | - "8" 20 | - "7" 21 | - "6" 22 | 23 | branches: 24 | only: 25 | - master 26 | 27 | notifications: 28 | email: 29 | - qiuzhongleiabc@126.com 30 | 31 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/page/Home.tsx.dtpl: -------------------------------------------------------------------------------- 1 | /** 2 | * Create by $user at $datetime 3 | * All right reserved 4 | */ 5 | 6 | import * as React from 'react' 7 | 8 | require('./style/${ModuleName}.css') 9 | 10 | export class ${ModuleName} extends React.Component { 11 | render() { 12 | return ( 13 |
14 | This is ${ModuleName} page 15 |
16 | ) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/render/dtpl.js: -------------------------------------------------------------------------------- 1 | module.exports = function(s) { 2 | 3 | return { 4 | templates: [ 5 | { 6 | name: 'r1.dtpl', 7 | matches: 'dtpl*' 8 | }, 9 | { 10 | name: 'r2.ejs', 11 | matches: 'ejs*' 12 | }, 13 | { 14 | name: 'r3.njk', 15 | matches: 'njk*' 16 | }, 17 | { 18 | name: 'r4.txt', 19 | matches: 'txt*' 20 | } 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/widget/Footer.tsx.dtpl: -------------------------------------------------------------------------------- 1 | /** 2 | * Create by $user at $datetime 3 | * All right reserved 4 | */ 5 | 6 | import * as React from 'react' 7 | 8 | export class ${ModuleName} extends React.Component { 9 | render() { 10 | return ( 11 |
12 | This Project is auto created by dot-template 13 |
14 | ) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/dot-template-types/common/transformer.d.ts: -------------------------------------------------------------------------------- 1 | export declare const transformer: { 2 | /** 3 | * hello-world => helloWorld 4 | */ 5 | camel(str: string): string; 6 | /** 7 | * hello-world => HelloWorld 8 | */ 9 | capitalize(str: string): string; 10 | /** 11 | * hello-world => HELLO_WORLD 12 | */ 13 | upper(str: string): string; 14 | /** 15 | * hello-world => hello_world 16 | */ 17 | snake(str: string): string; 18 | }; 19 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/res/snippets/inject-docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "inject": { 3 | "prefix": "inject", 4 | "body": "/*# INJECT_START {\"key\": \"${1:key}\"} #*/\n/*# INJECT_END #*/", 5 | "description": "the tags of using mora-script to inject content" 6 | }, 7 | "inject react comment": { 8 | "prefix": "injectr", 9 | "body": "{/*# INJECT_START {\"key\": \"${1:key}\"} #*/}\n{/*# INJECT_END #*/}", 10 | "description": "the tags of using mora-script to inject content" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.gitmessage: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Format: (): 4 | # 5 | # Types: feat, fix, docs, style, refactor, perf, test, chore, revert 6 | # Changelog: Added, Changed, Breaks, Deprecated, Removed, Fixed, Security 7 | # 8 | # Guides: http://chris.beams.io/posts/git-commit/ 9 | # 10 | # Example: 11 | # fix($compile): couple of unit tests for IE9 12 | # 13 | # Older IEs serialize html uppercased, but IE9 does not... 14 | # 15 | # Closes #392, #400 16 | # Breaks foo.bar api, foo.baz should be used instead# 17 | # 18 | -------------------------------------------------------------------------------- /packages/dot-template-cli/src/adapter/CliEditor.ts: -------------------------------------------------------------------------------- 1 | import {Editor} from 'dot-template-core' 2 | import * as inquirer from 'inquirer' 3 | 4 | export class CliEditor extends Editor { 5 | 6 | constructor(rootPath: string, debug: boolean = false) { 7 | super(rootPath) 8 | this.configuration.debug = debug 9 | } 10 | 11 | async confirm(message: string): Promise { 12 | let answer = await inquirer.prompt({message, type: 'confirm', name: 'chose'}) 13 | return answer.chose 14 | } 15 | 16 | dispose() { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/commands/CreateDirectoriesCommand.d.ts: -------------------------------------------------------------------------------- 1 | import { Command, ICommandInitOptions } from './Command'; 2 | import { Application } from '../Application'; 3 | export declare class CreateDirectoriesCommand extends Command { 4 | private folders; 5 | private exists; 6 | constructor(folders: string[], noInitError: boolean, app: Application, options: ICommandInitOptions); 7 | execute(): Promise; 8 | rollback(): Promise; 9 | remove(dir: string, removeCurrent?: boolean): Promise; 10 | } 11 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/res/syntaxes/dtpl.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "Dot Template", 4 | "patterns": [ 5 | { 6 | "include": "#keywords" 7 | } 8 | ], 9 | "repository": { 10 | "keywords": { 11 | "patterns": [ 12 | { 13 | "name": "keyword.unquoted.dtpl", 14 | "match": "(\\$[a-zA-Z][\\w]*)\\b" 15 | }, 16 | { 17 | "name": "keyword.quoted.dtpl", 18 | "match": "(\\$\\{[a-zA-Z][\\w\\.]*\\})" 19 | } 20 | ] 21 | } 22 | }, 23 | "scopeName": "source.dtpl" 24 | } 25 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/config/tsconfig.json.dtpl: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": [ 5 | "dom", 6 | "es5" 7 | ], 8 | "noLib": false, 9 | "module": "commonjs", 10 | "declaration": false, 11 | "noImplicitAny": false, 12 | "removeComments": true, 13 | "experimentalDecorators": true, 14 | "noUnusedLocals": false, 15 | "allowSyntheticDefaultImports": true, 16 | "moduleResolution": "node", 17 | "jsx": "react" 18 | }, 19 | "include": [ 20 | "${ref.fileName}" 21 | ], 22 | "exclude": [ 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # Copy from https://github.com/bcoe/nyc/blob/master/appveyor.yml 2 | environment: 3 | matrix: 4 | - nodejs_version: '6' 5 | - nodejs_version: '7' 6 | - nodejs_version: '8' 7 | install: 8 | - ps: Install-Product node $env:nodejs_version 9 | - set CI=true 10 | - npm -g install npm@latest 11 | - npm -g install lerna 12 | - set PATH=%APPDATA%\npm;%PATH% 13 | - npm install 14 | - lerna bootstrap 15 | - npm test 16 | 17 | matrix: 18 | fast_finish: true 19 | build: off 20 | version: '{build}' 21 | shallow_clone: true 22 | clone_depth: 1 23 | test_script: 24 | - node --version 25 | - npm --version 26 | - npm test 27 | -------------------------------------------------------------------------------- /dot-template.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | }, 6 | { 7 | "path": "packages/dot-template-core" 8 | }, 9 | { 10 | "path": "packages/dot-template-vscode" 11 | }, 12 | { 13 | "path": "packages/dot-template-cli" 14 | }, 15 | { 16 | "path": "packages/dot-template-types" 17 | } 18 | ], 19 | "settings": { 20 | "files.exclude": { 21 | "*.code-workspace": true, 22 | "packages": true, 23 | "**/.git": true, 24 | "**/.svn": true, 25 | "**/.hg": true, 26 | "**/CVS": true, 27 | "**/.DS_Store": true 28 | }, 29 | "typescript.tsdk": "dot-template-core/node_modules/typescript/lib" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/inc/helper.ts: -------------------------------------------------------------------------------- 1 | import * as findup from 'mora-scripts/libs/fs/findup' 2 | import * as path from 'path' 3 | import * as assert from 'assert' 4 | import * as fs from 'fs-extra' 5 | 6 | export const rootPath = path.dirname(findup.pkg()) 7 | export const fixturesPath = path.resolve(rootPath, 'src/test/core/fixtures') 8 | 9 | export async function delay(timeout: number) { 10 | return new Promise((resolve) => { 11 | setTimeout(resolve, timeout) 12 | }) 13 | } 14 | 15 | export {path, assert, fs} 16 | 17 | export * from '../../../core/common' 18 | export * from '../../../core/Application' 19 | export * from '../../../adapter/TestEditor' 20 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "Launch Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceRoot}/packages/dot-template-vscode" ], 14 | "stopOnEntry": false, 15 | "sourceMaps": true, 16 | "outFiles": [ "${workspaceRoot}/packages/dot-template-vscode/out/**/*.js" ] 17 | }, 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/index.html.dtpl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | ${ref.fileName} 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/dot-template-cli/README.md: -------------------------------------------------------------------------------- 1 | # dot-template-cli 2 | 3 | dot-template 命令行工具,vscode 中可以安装 [dot-template vscode 插件](https://marketplace.visualstudio.com/items?itemName=qiu8310.dot-template-vscode) 4 | 5 | ## 使用方法 6 | 7 | ### 单独使用 8 | 9 | * 使用 `dtpl touch ` 创建文本文件; 10 | * 使用 `dtpl related ` 创建指定的文件的关联文件; 11 | * 使用 `dtpl mkdir ` 创建文件夹; 12 | 13 | ### 创建服务器后再使用(方便撤销和重做) 14 | 15 | 1. 使用 `dtpl watch` 创建一个服务器 16 | 2. 使用 `dtpl touch ` 创建文本文件; 17 | 使用 `dtpl related ` 创建指定的文件的关联文件; 18 | 使用 `dtpl mkdir ` 创建文件夹; 19 | 使用 `dtpl revoke` 来撤销或重做上一次命令(只能撤销一次,并且要在 1 分钟内,因为文件很容易被更新,撤销容易丢失文件) 20 | 21 | 22 | ## 更多详情请查看 [dot-template](https://github.com/qiu8310/dot-template) 23 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/file/Template.d.ts: -------------------------------------------------------------------------------- 1 | import { ICopySource, ICopyFilterResult, ICopiedFiles, IUserTemplate, IData, IRelated, IInject } from '../common'; 2 | import { Source } from './Source'; 3 | import { Application } from '../Application'; 4 | export declare class Template { 5 | source: Source; 6 | filePath: string; 7 | data: IData; 8 | custom: IUserTemplate; 9 | app: Application; 10 | constructor(source: Source, filePath: string, data: IData, custom: IUserTemplate); 11 | filter(copySource: ICopySource): boolean | ICopyFilterResult; 12 | afterFilter(fromDir: string, toDir: string, result: ICopiedFiles): void; 13 | getRelatedSources(): IRelated[]; 14 | getInjects(): IInject[]; 15 | } 16 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/top-no-example/dtpl.cjs.ejs: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | /* 3 | 1. 在此脚本中使用 console 语句是不会输出在控制面板的,因为此脚本是在 4 | vscode 插件中执行的,插件的输出不在当前环境中;不过你可以设置配置 5 | 项中的 dot-template-vscode.debug 为 true,并在此程序中执行: 6 | 7 | source.app.debug('...') 8 | 9 | 2. 当 matches 是字符串时,可以只匹配 basename,但如果 matches 带 10 | 路径时,就要从项目根路径开始匹配,否则无法匹配成功。(主要是是因为 11 | minimatch 的选项 matchBase 设置为 true 了,你可以用 12 | dot-template-vscode.minimatchOptions 来修改默认的配置) 13 | */ 14 | 15 | /** 16 | * @param {import('./dtpl').Source} source 17 | * @returns {import('./dtpl').IDtplConfig} 18 | */ 19 | function config (source) { 20 | return { 21 | templates: [ 22 | ], 23 | globalData: { 24 | } 25 | }; 26 | } 27 | 28 | module.exports = config 29 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/Render.d.ts: -------------------------------------------------------------------------------- 1 | import { Application } from './Application'; 2 | import { IObject } from './common'; 3 | export declare enum Engine { 4 | dtpl = 1, 5 | ejs = 2, 6 | njk = 3 7 | } 8 | export declare class Render { 9 | private app; 10 | constructor(app: Application); 11 | judgeEngineByFileExtension(file: string): Engine | null; 12 | removeFileEngineExtension(file: string): string; 13 | renderFile(file: string, data: IObject): string; 14 | renderContent(content: string, data: IObject, engine: Engine | null): string; 15 | renderDtplContent(content: string, data: IObject): string; 16 | renderEjsContent(content: string, data: IObject): string; 17 | renderNjkContent(content: string, data: IObject): string; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "out", 6 | "noUnusedLocals": true, 7 | "noImplicitAny": true, 8 | "noUnusedParameters": false, 9 | "noImplicitThis": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "strictPropertyInitialization": true, 15 | "experimentalDecorators": true, 16 | 17 | "declaration": true, 18 | "pretty": true, 19 | "importHelpers": true, 20 | 21 | "lib": [ 22 | "es2016" 23 | ], 24 | "sourceMap": true 25 | }, 26 | "include": [ 27 | "src" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | "packages" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /packages/dot-template-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "out", 6 | "noUnusedLocals": true, 7 | "noImplicitAny": true, 8 | "noUnusedParameters": false, 9 | "noImplicitThis": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "strictPropertyInitialization": true, 15 | "experimentalDecorators": true, 16 | 17 | "declaration": false, 18 | "pretty": true, 19 | "importHelpers": true, 20 | 21 | "lib": [ 22 | "es2016" 23 | ], 24 | "sourceMap": false 25 | }, 26 | "include": [ 27 | "src" 28 | ], 29 | "exclude": [ 30 | "node_modules" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/app/HoverProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import {Hover, TextDocument, Position, CancellationToken} from 'vscode' 3 | import {AppRelied} from './AppRelied' 4 | 5 | export class HoverProvider extends AppRelied implements vscode.HoverProvider { 6 | provideHover(document: TextDocument, position: Position, token: CancellationToken) { 7 | let range = document.getWordRangeAtPosition(position, /\$\{[\w\.]+\}|\$\w+\b/) 8 | if (!range) return null 9 | 10 | let word = document.getText(range).substr(1) // 去掉 $ 符号 11 | let data = this.data(document) 12 | 13 | let len = word.length 14 | if (word[0] === '{' && word[len - 1] === '}') word = word.substr(1, len - 2) 15 | 16 | return new Hover(this.markdown(word, data)) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "out", 6 | "noUnusedLocals": true, 7 | "noImplicitAny": true, 8 | "noUnusedParameters": false, 9 | "noImplicitThis": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "strictPropertyInitialization": true, 15 | "experimentalDecorators": true, 16 | 17 | "declaration": true, 18 | "pretty": true, 19 | "importHelpers": true, 20 | 21 | "lib": [ 22 | "es2016" 23 | ], 24 | "sourceMap": true 25 | }, 26 | "include": [ 27 | "src" 28 | ], 29 | "exclude": [ 30 | "node_modules", 31 | ".vscode-test" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/.dtpl/dtpl.js: -------------------------------------------------------------------------------- 1 | module.exports = function(source) { 2 | return { 3 | templates: [ 4 | { 5 | name: 'upper.dtpl', 6 | matches: ['upper*'] 7 | }, 8 | { 9 | name: 'variable.dtpl', 10 | matches: ['var-yes*'], 11 | localData: { 12 | foo: {bar: 'foobar'} 13 | } 14 | }, 15 | { 16 | name: 'variable.dtpl', 17 | matches: ['var-no*'], 18 | localData: {foo: 'foo', bar: 'bar'} 19 | }, 20 | { 21 | name: 'text.txt', 22 | matches: ['text*'] 23 | }, 24 | { 25 | name: 'folder', 26 | matches: 'folder*' 27 | }, 28 | { 29 | name: 'not-exists-file', 30 | matches: 'noexists*' 31 | } 32 | ] 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | // import * as vscode from 'vscode'; 12 | // import * as myExtension from '../extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", () => { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", () => { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/test_render.ts: -------------------------------------------------------------------------------- 1 | import {Project, File} from './inc/' 2 | 3 | let pro: Project 4 | 5 | describe('render', () => { 6 | beforeEach(() => pro = new Project('render', 'render')) 7 | afterEach(() => pro.destroy()) 8 | 9 | it('dtpl', async () => { 10 | let f = new File('dtpl-test', pro) 11 | await f.create(true) 12 | f.shouldMatch('DTPL_TEST') 13 | }) 14 | it('ejs', async () => { 15 | let f = new File('ejs-test', pro) 16 | await f.create(true) 17 | f.shouldMatch('EJS_TEST') 18 | }) 19 | it('njk', async () => { 20 | let f = new File('njk-test', pro) 21 | await f.create(true) 22 | f.shouldMatch('NJK_TEST') 23 | }) 24 | it('no', async () => { 25 | let f = new File('txt-test', pro) 26 | await f.create(true) 27 | f.shouldMatch('MODULE_NAME') 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/dot-template-core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es5", 5 | "outDir": "out", 6 | "noUnusedLocals": true, 7 | "noImplicitAny": true, 8 | "noUnusedParameters": false, 9 | "noImplicitThis": true, 10 | "noFallthroughCasesInSwitch": true, 11 | "noImplicitReturns": true, 12 | "strict": true, 13 | "strictNullChecks": true, 14 | "strictPropertyInitialization": true, 15 | "experimentalDecorators": true, 16 | 17 | "declaration": true, 18 | "pretty": true, 19 | "importHelpers": true, 20 | 21 | "lib": [ 22 | "es2016" 23 | ], 24 | // "rootDir": "src", // 指定 rootDir 会使 ts 在编译外部的 dtpl.ts 文件报错 25 | "sourceMap": true 26 | }, 27 | "include": [ 28 | "src" 29 | ], 30 | "exclude": [ 31 | "node_modules" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/commands/CreateRelatedFilesCommand.d.ts: -------------------------------------------------------------------------------- 1 | import { Command, ICommandInitOptions } from './Command'; 2 | import { Application } from '../Application'; 3 | export interface IPoint { 4 | row: number; 5 | col: number; 6 | } 7 | export declare class CreateRelatedFilesCommand extends Command { 8 | private relatedSources; 9 | private infos; 10 | /** 11 | * 初始文件的 Source 12 | */ 13 | private source; 14 | /** 15 | * 创建关联文件 16 | * 17 | * relatedFiles 所关联的文件都是不存在的 18 | */ 19 | constructor(textFile: string, app: Application, options: ICommandInitOptions); 20 | private replace; 21 | execute(): Promise; 22 | rollback(): Promise; 23 | } 24 | export declare function calculateStartInjectPoint(content: string, reference: string): { 25 | begin: IPoint; 26 | end?: IPoint; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "scripts": { 3 | "dev": "webpack-dev-server --hot", 4 | "build": "NODE_ENV=production webpack -p" 5 | }, 6 | "dependencies": { 7 | "react": "~16.0.0", 8 | "react-dom": "~16.0.0", 9 | "react-router": "~4.2.0", 10 | "react-router-dom": "~4.2.2" 11 | }, 12 | "devDependencies": { 13 | "dot-template-types": "^0.0.1", 14 | "@types/react": "~16.0.19", 15 | "@types/react-dom": "~16.0.2", 16 | "@types/react-router": "~4.0.16", 17 | "@types/react-router-dom": "~4.2.0", 18 | "awesome-typescript-loader": "~3.3.0", 19 | "css-loader": "~0.28.7", 20 | "extract-text-webpack-plugin": "~3.0.2", 21 | "style-loader": "~0.19.0", 22 | "ts-node": "^4.1.0", 23 | "typescript": "~2.7.1", 24 | "webpack": "~3.8.1", 25 | "webpack-dev-server": "~2.9.4" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "--dot-template--", 3 | "private": true, 4 | "license": "MIT", 5 | "scripts": { 6 | "prepublish": "npm run build", 7 | "publish": "lerna publish --registry https://registry.npmjs.org/", 8 | "clear": "remove-all out", 9 | "build": "npm run clear && tsc -p ./", 10 | "watch": "npm run clear && tsc -watch -p ./", 11 | "test": "lerna run test --ignore dot-template-vscode" 12 | }, 13 | "engines": { 14 | "node": "^8.0.0" 15 | }, 16 | "config": { 17 | "hooks": { 18 | "commit-msg": true, 19 | "post-merge": true, 20 | "pre-push": "npm run test" 21 | } 22 | }, 23 | "devDependencies": { 24 | "@types/fs-extra": "~5.0.0", 25 | "@types/node": "^8.9.0", 26 | "fs-extra": "~5.0.0", 27 | "lerna": "^2.11.0", 28 | "mora-scripts": "~1.6.20", 29 | "ts-node": "^4.1.0", 30 | "tslib": "^1.9.0", 31 | "typescript": "^2.7.1" 32 | }, 33 | "dependencies": {} 34 | } 35 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/test_calculateStartInjectPoint.ts: -------------------------------------------------------------------------------- 1 | import {calculateStartInjectPoint} from '../../core/commands/CreateRelatedFilesCommand' 2 | import * as assert from 'assert' 3 | 4 | 5 | let c1 = ` 6 | import * as React from 'react' 7 | import {Widget} from '@hujiang/foe-common' 8 | 9 | export namespace ControlContainer { 10 | export interface Props extends Widget.InjectProps { 11 | foo: string 12 | } 13 | 14 | export type State = typeof initState 15 | } 16 | 17 | const initState = {} 18 | 19 | /** 表单控件的容器 */ 20 | export class ControlContainer extends React.PureComponent { 21 | state: ControlContainer.State = initState 22 | } 23 | ` 24 | 25 | describe('calculateStartInjectPoint', () => { 26 | it('c1', () => { 27 | assert.deepEqual(calculateStartInjectPoint(c1, 'import x'), {begin: {row: 3, col: 0}}) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | } 4 | main { 5 | display: flex; 6 | } 7 | 8 | h1 { 9 | padding: 0 30px; 10 | line-height: 80px; 11 | height: 80px; 12 | text-align: center; 13 | margin: 0; 14 | } 15 | @media (max-width: 500px) { 16 | h1 { 17 | font-size: 18px; 18 | } 19 | } 20 | 21 | aside { 22 | padding: 0 30px; 23 | width: 100px; 24 | min-height: calc(100vh - 130px); 25 | } 26 | 27 | aside a { 28 | display: block; 29 | line-height: 30px; 30 | color: lightskyblue; 31 | } 32 | aside a.active { 33 | pointer-events: none; 34 | color: rgb(99, 98, 98); 35 | text-decoration: none; 36 | } 37 | 38 | section { 39 | flex: auto; 40 | } 41 | 42 | footer { 43 | height: 50px; 44 | line-height: 50px; 45 | text-align: center; 46 | background-color: whitesmoke; 47 | color: #AAA; 48 | } 49 | 50 | .welcome { 51 | line-height: 60px; 52 | font-size: 1.4em; 53 | } 54 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/commands/CreateTemplateFilesCommand.d.ts: -------------------------------------------------------------------------------- 1 | import { Command, ICommandInitOptions } from './Command'; 2 | import { Application } from '../Application'; 3 | /** 4 | * 创建文件并注入模板的命令 5 | * 6 | * **注意:** 7 | * 8 | * - 文件之前是不存在的 9 | * - 可以指定多个文件 10 | * 11 | */ 12 | export declare class CreateTemplateFilesCommand extends Command { 13 | private open; 14 | files: string[]; 15 | /** 16 | * 对应文件的创建前的信和,在撤消命令时,要判断内容是否改过了,改过了要弹出确认框 17 | */ 18 | private infos; 19 | /** 20 | * @param {string[]} files 所有要创建的文件绝对路径,一定要确保文件不存在 21 | * @param {boolean} [open] 是否要打开这些创建好的文件 22 | * @param {boolean} [noInitError] 初始化时不要报错,主要 emitNewFile 时可能是因为用户修改了文件夹的名称 23 | * @memberof CreateFilesCommand 24 | */ 25 | constructor(files: string[], open: boolean, noInitError: boolean, app: Application, options: ICommandInitOptions); 26 | execute(): Promise; 27 | rollback(): Promise; 28 | } 29 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/vscode.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import {App, AutoCompletion, HoverProvider} from './app/' 3 | 4 | export function activate(context: vscode.ExtensionContext) { 5 | const dtplDocumentSelector = {scheme: 'file', language: 'dtpl'} 6 | const app = new App() 7 | 8 | context.subscriptions.push( 9 | app, 10 | vscode.languages.registerHoverProvider(dtplDocumentSelector, new HoverProvider()), 11 | vscode.languages.registerCompletionItemProvider(dtplDocumentSelector, new AutoCompletion(), '$', '.', '${'), 12 | 13 | /*# INJECT_START commands #*/ 14 | vscode.commands.registerCommand('dot-template-vscode.createTemplateFiles', app.createTemplateFiles), 15 | vscode.commands.registerCommand('dot-template-vscode.createRelatedFiles', app.createRelatedFiles), 16 | vscode.commands.registerCommand('dot-template-vscode.undoOrRedo', app.undoOrRedo) 17 | /*# INJECT_END #*/ 18 | ) 19 | 20 | return app 21 | } 22 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/test_undoTimeout.ts: -------------------------------------------------------------------------------- 1 | import {Project, File, delay, assert} from './inc' 2 | 3 | describe('timeout', () => { 4 | it('undo timeout error', async () => { 5 | let p = new Project('timeout') 6 | p.editor.configuration.commandInvalidTimeout = 10 7 | let f = new File('xx', p) 8 | await f.create(true) 9 | await delay(20) 10 | 11 | try { 12 | await p.unredoAsync(false) 13 | } catch (e) { 14 | 15 | assert(e.message.indexOf('命令已经过期') > 0) 16 | } 17 | p.destroy() 18 | }) 19 | 20 | it('redo timeout error', async () => { 21 | let p = new Project('timeout') 22 | p.editor.configuration.commandInvalidTimeout = 30 23 | let f = new File('xx', p) 24 | await f.create(true) 25 | await p.unredoAsync(true) 26 | 27 | await delay(50) 28 | try { 29 | await p.unredoAsync(false) 30 | } catch (e) { 31 | assert(e.message.indexOf('命令已经过期') > 0) 32 | } 33 | p.destroy() 34 | }) 35 | }) 36 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/index.tsx.dtpl: -------------------------------------------------------------------------------- 1 | import * as ReactDOM from 'react-dom' 2 | import * as React from 'react' 3 | 4 | import {HashRouter, Route, NavLink} from 'react-router-dom' 5 | 6 | import {Home} from './page/Home' 7 | import {About} from './page/About' 8 | import {Footer} from './widget/Footer' 9 | 10 | require('./index.css') 11 | 12 | ReactDOM.render(( 13 | 14 |
15 |

dot-template: ${ref.fileName}

16 |
17 | 21 |
22 |
Welcome to ${ref.fileName} !
} /> 23 | 24 | 25 |
26 |
27 |
28 |
29 |
30 | ), document.getElementById('root')) 31 | -------------------------------------------------------------------------------- /packages/dot-template-types/common/helper.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 按先后顺序一个个用 run 函数来运行 tasks 中的字段 3 | * 4 | * @export 5 | * @template T 6 | * @template R 7 | * @param {T[]} tasks 要运行的任务 8 | * @param {(task: T) => Promise} run 运行函数 9 | * @returns {Promise} 返回每个 tasks 对应的结果组成的数组 10 | */ 11 | export declare function series(tasks: T[], run: (task: T, index: number, tasks: T[]) => Promise): Promise; 12 | /** 13 | * 对数组去重 14 | */ 15 | export declare function unique(items: T[], uniqueKey?: K): T[]; 16 | /** 17 | * 查找 js 文件中引用的其它文件,一般是通过 require 或 import 语法来引用的 18 | * 19 | * 如: 20 | * 21 | * ``` 22 | * import Test from './Test' 23 | * export * from './Test' 24 | * const Test = require('./Test') 25 | * import Test = require('./Test') 26 | * ``` 27 | */ 28 | export declare function findJsRelatedFiles(jsfile: string, fileContent: string): string[]; 29 | export declare function requireFile(file: string): any; 30 | export declare function getIgnore(rootPath: string): any; 31 | export declare function toArray(item: undefined | T | T[]): T[]; 32 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/test/index.ts: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | import * as testRunner from 'vscode/lib/testrunner'; 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; 23 | -------------------------------------------------------------------------------- /packages/dot-template-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dot-template-cli", 3 | "description": "create new files according to pre-defined templates", 4 | "version": "0.3.0", 5 | "license": "MIT", 6 | "scripts": { 7 | "prepublish": "npm run build", 8 | "clear": "remove-all out", 9 | "build": "npm run clear && tsc -p ./", 10 | "watch": "npm run clear && tsc -watch -p ./" 11 | }, 12 | "bin": { 13 | "dtpl": "out/cli.js" 14 | }, 15 | "keywords": [ 16 | "dtpl", 17 | "template", 18 | "file", 19 | "variable" 20 | ], 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/qiu8310/dot-template" 24 | }, 25 | "devDependencies": { 26 | "@types/chokidar": "~1.7.3", 27 | "@types/fs-extra": "~5.0.0", 28 | "@types/inquirer": "0.0.37", 29 | "@types/node": "8.9.0", 30 | "ts-node": "^4.1.0", 31 | "typescript": "^2.7.1" 32 | }, 33 | "dependencies": { 34 | "chokidar": "~2.0.0", 35 | "dot-template-core": "^0.2.0", 36 | "fs-extra": "~5.0.0", 37 | "inquirer": "~5.1.0", 38 | "mora-scripts": "~1.6.20", 39 | "tslib": "^1.9.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/template/example/config/webpack.config.js.dtpl: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | const ExtractTextPlugin = require('extract-text-webpack-plugin') 3 | 4 | const root = path.resolve(__dirname) 5 | const src = path.resolve(root, '${ref.fileName}') 6 | const dist = path.resolve(root, 'dist') 7 | 8 | const DEV = process.env.NODE_ENV !== 'production' 9 | 10 | module.exports = { 11 | target: 'web', 12 | entry: { 13 | index: path.resolve(src, 'index.tsx') 14 | }, 15 | output: { 16 | path: dist, 17 | publicPath: '', 18 | filename: '[name].js' 19 | }, 20 | 21 | resolve: { 22 | extensions: ['.js', '.jsx', '.ts', '.tsx'] 23 | }, 24 | plugins: [ 25 | new ExtractTextPlugin({ 26 | filename: '[name].css', 27 | disable: DEV 28 | }) 29 | ], 30 | module: { 31 | rules: [ 32 | {test: /\.tsx?$/, use: 'awesome-typescript-loader'}, 33 | {test: /\.css$/, use: ExtractTextPlugin.extract({fallback: 'style-loader', use: [{loader: 'css-loader'}]})} 34 | ] 35 | }, 36 | devServer: { 37 | contentBase: src, 38 | historyApiFallback: true 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/file/Source.d.ts: -------------------------------------------------------------------------------- 1 | import { Template } from './Template'; 2 | import { Application } from '../Application'; 3 | import { IBasicData, IData, IUserTemplate } from '../common'; 4 | export declare class Source { 5 | app: Application; 6 | filePath: string; 7 | private _basicData?; 8 | relativeFilePath: string; 9 | exists: boolean; 10 | isFile: boolean; 11 | isDirectory: boolean; 12 | fileContent: string; 13 | systemConfigDir: string; 14 | constructor(app: Application, filePath: string); 15 | readonly basicData: IBasicData; 16 | createTemplate(filePath: string, data: IData, userTemplate: IUserTemplate): Template; 17 | match(isTemplateDirectory: boolean): Template | undefined; 18 | /** 19 | * 根据用户的配置,查找一个匹配的并且存在的模板文件 20 | */ 21 | private findMatchedUserTemplate; 22 | /** 23 | * 加载配置文件,每次都重新加载,确保无缓存 24 | */ 25 | private loadDtplConfig; 26 | /** 27 | * 在 dtpl 目录内找到配置文件 28 | */ 29 | private findConfigFileInDtplFolder; 30 | /** 31 | * 不在递归向上查找 .dtpl 文件夹了(因为如果两个编辑器打开的项目共用一个 .dtpl 文件夹时,会出现问题) 32 | */ 33 | private findAllDirectoriesCanExistsDtplFolder; 34 | } 35 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * [x] 指定监听的文件,(避免去监听 node_modules 下的文件或其它通用的地方) 2 | * [x] 监听 dtpl.ts 文件,文件为空时可以注入一个简化版本的 dtpl.ts 3 | * [x] 自动过滤 .gitignore 中的文件(处理文件夹还不太完美,需要根据后面有没 / 来判断是否是文件夹,另外还不支持 ! 符号) 4 | 5 | * 改善 example 6 | 7 | * [x] related 文件支持 inject 一些信息到原文件上,inject 支持 append 8 | * [x] 自动更新 dtpl.ts 配置文件中的 interface 路径(或者发布一个 interface 包?) 9 | 10 | ``` 11 | import * as _ from '/Users/Mora/.vscode/extensions/qiu8310.dot-template-0.2.1/out/common/interface' 12 | ``` 13 | 14 | **采用了 dot-template-types 包的形式** 15 | 16 | 17 | * 完善 命令的前进后退机制 (DO & UNDO) 18 | * 打开(最好以预览的方式打开) .dtpl 目录下的 readme 文件(和下面的这条只要有一个存在就好) 19 | * 新建完目录时,最好展开它 20 | 21 | // 先显示 explorer 窗口 22 | vscode.commands.executeCommand('workbench.view.explorer') 23 | 24 | * 创建文件夹时,可以通过 filter 把文件复制到外面的一个目录里去,这时 revoke 无法删除它 25 | * 在线获取 template 模板(包括文件和目录) 26 | * 添加 vscode 的右键命令,即当鼠标放在右侧的文件或文件夹上时 27 | * 优先使用用户本地的 ts-node 28 | 29 | * 支持配置渲染引擎 30 | * 扩展 `.dtpl` 模板的功能 31 | * 添加 vscode 测试 和 cli 测试 32 | * 给 .dtpl 模板文件添加一个 icon 33 | 34 | 35 | 官方示例大全 https://github.com/Microsoft/vscode-extension-samples 36 | 37 | ## 发布代码步骤 38 | 39 | ```bash 40 | 41 | git checkout master 42 | 43 | npm run build 44 | npm test 45 | npm version patch/minor/major 46 | 47 | cd typing 48 | # 修改 package.json 的版本号 49 | npm publish 50 | cd .. 51 | 52 | git checkout vscode 53 | git merge master 54 | vsce publish 55 | 56 | git checkkout npm 57 | git merge master 58 | npm publish 59 | 60 | ``` 61 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/js/dtpl.js: -------------------------------------------------------------------------------- 1 | module.exports = function() { 2 | return { 3 | templates: [ 4 | { 5 | name: 'dir', 6 | matches: 'dir*' 7 | }, 8 | { 9 | name: 'dir', 10 | matches: 'filter-text', 11 | filter(copy) { 12 | return copy.rawName === 'text.txt' 13 | } 14 | }, 15 | { 16 | name: 'dir', 17 | matches: 'filter-content', 18 | filter(copy) { 19 | return copy.rawName === 'text.txt' ? {content: 'hack'} : false 20 | } 21 | }, 22 | { 23 | name: 'dir', 24 | matches: 'filter-name', 25 | filter(copy) { 26 | return copy.rawName === 'text.txt' ? {name: 'hack'} : false 27 | } 28 | }, 29 | { 30 | name: 'dir', 31 | matches: 'filter-content-name', 32 | filter(copy) { 33 | return copy.rawName === 'text.txt' ? {content: 'hack', name: 'hack'} : false 34 | } 35 | }, 36 | { 37 | name: 'dir', 38 | matches: 'filter-path', 39 | filter(copy) { 40 | return copy.rawName === 'text.txt' ? {filePath: 'hack'} : false 41 | } 42 | }, 43 | { 44 | name: 'dir', 45 | matches: 'filter-path-invalid', 46 | filter(copy) { 47 | return copy.rawName === 'text.txt' ? {filePath: './js/dir/hack'} : false 48 | } 49 | }, 50 | ] 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/Commander.d.ts: -------------------------------------------------------------------------------- 1 | import { Command } from './commands/Command'; 2 | import { Application } from './Application'; 3 | export declare class Commander { 4 | app: Application; 5 | length: number; 6 | history: Command[]; 7 | /** 8 | * 收集所有下次要运行的命令 9 | * 10 | * 系统需要保存命令一个一个的运行 11 | */ 12 | private queue; 13 | private isRunning; 14 | private isChecking; 15 | private lastRunningTime?; 16 | /** 17 | * 指向 prev 操作要执行的命令的索引 18 | */ 19 | cursor: number; 20 | constructor(app: Application, length: number); 21 | private runCommand; 22 | fileMaybeCreatedByCommand(): boolean | 0 | undefined; 23 | private add; 24 | private getCommonComamndOpts; 25 | private wrap; 26 | addCreateDirectoriesCommand(folders: string[], noInitError: boolean): Promise; 27 | addCreateRelatedFilesCommand(textFile: string): Promise; 28 | addCreateTemplateFilesCommand(textFiles: string[], open: boolean, noInitError: boolean): Promise; 29 | readonly hasNext: boolean; 30 | readonly hasPrev: boolean; 31 | /** 32 | * 执行历史记录中的下一条命令 33 | * 34 | * @returns {Promise} 如果没有下一条或者命令执行失败,返回 false 35 | * @memberof Commander 36 | */ 37 | next(): Promise; 38 | /** 39 | * 执行历史记录中的上一条命令 40 | * 41 | * @returns {Promise} 如果没有上一条或者命令执行失败,返回 false 42 | * @memberof Commander 43 | */ 44 | prev(): Promise; 45 | } 46 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/app/AutoCompletion.ts: -------------------------------------------------------------------------------- 1 | import {CompletionItemProvider, TextDocument, Position, CompletionItem, Range, CompletionItemKind} from 'vscode' 2 | import {AppRelied} from './AppRelied' 3 | import * as DotProp from 'mora-scripts/libs/lang/DotProp' 4 | 5 | const variableRegexp = /\$\{?([\w\.]*)$/ 6 | 7 | export class AutoCompletion extends AppRelied implements CompletionItemProvider { 8 | 9 | provideCompletionItems(document: TextDocument, position: Position): CompletionItem[] { 10 | const start: Position = new Position(position.line, 0) 11 | const range: Range = new Range(start, position) 12 | const text: string = document.getText(range) 13 | const matches = text.match(variableRegexp) 14 | if (!matches) return [] 15 | 16 | let data = this.data(document) 17 | let isKeyTop = true 18 | 19 | let rawWords = matches[1] 20 | let prefix: string | undefined 21 | if (matches[0].indexOf('{') >= 0 && rawWords.indexOf('.') > 0) { 22 | isKeyTop = false 23 | let parts = rawWords.split('.') 24 | prefix = parts.pop() 25 | data = DotProp.get(data, parts.join('.')) 26 | if (typeof data !== 'object') return [] 27 | } 28 | 29 | return Object.keys(data || {}) 30 | .filter(k => !prefix || k.startsWith(prefix)) 31 | .map(k => { 32 | let c = new CompletionItem(k, CompletionItemKind.Variable) 33 | 34 | c.documentation = this.doc(k, isKeyTop, data) 35 | c.detail = this.detail(k, isKeyTop, data) 36 | return c 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/test_common.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert' 2 | 3 | import {series} from '../../common/helper' 4 | import {transformer} from '../../common/transformer' 5 | 6 | describe('helper', () => { 7 | it('series a empty array', async () => { 8 | assert.deepEqual(await series([], async (any) => any), []) 9 | }) 10 | it('series a number array', async () => { 11 | assert.deepEqual(await series([1, 2, 3], async (num) => num * num), [1, 4, 9]) 12 | }) 13 | }) 14 | 15 | describe('transformer', () => { 16 | it('camel', () => { 17 | assert.equal(transformer.camel(''), '') 18 | assert.equal(transformer.camel('aaa'), 'aaa') 19 | assert.equal(transformer.camel('a b'), 'aB') 20 | assert.equal(transformer.camel('A b'), 'aB') 21 | assert.equal(transformer.camel('中a'), 'a') 22 | assert.equal(transformer.camel('我 a'), 'a') 23 | }) 24 | 25 | it('capitalize', () => { 26 | assert.equal(transformer.capitalize(''), '') 27 | assert.equal(transformer.capitalize('aaa'), 'Aaa') 28 | assert.equal(transformer.capitalize('a b'), 'AB') 29 | assert.equal(transformer.capitalize('A B'), 'AB') 30 | assert.equal(transformer.capitalize('hello w'), 'HelloW') 31 | }) 32 | 33 | it('snake', () => { 34 | assert.equal(transformer.snake('hello-world'), 'hello_world') 35 | assert.equal(transformer.snake('Hello world'), 'hello_world') 36 | assert.equal(transformer.snake('HelloWorld'), 'hello_world') 37 | }) 38 | 39 | it('upper', () => { 40 | assert.equal(transformer.upper('aBc'), 'A_BC') 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/common/transformer.ts: -------------------------------------------------------------------------------- 1 | /** Used to match words to create compound words. */ 2 | const reWords = (function() { 3 | const upper = '[A-Z\\xc0-\\xd6\\xd8-\\xde]' 4 | const lower = '[a-z\\xdf-\\xf6\\xf8-\\xff]+' 5 | 6 | return RegExp(upper + '+(?=' + upper + lower + ')|' + upper + '?' + lower + '|' + upper + '+|[0-9]+', 'g') 7 | }()) 8 | 9 | function wrap(str: string, fn: (prevValue: string, currentValue: string, currentIndex: number, ref: string[]) => string): string { 10 | let matches = str.match(reWords) 11 | return matches ? matches.reduce(fn, '') : str 12 | } 13 | 14 | export const transformer = { 15 | /** 16 | * hello-world => helloWorld 17 | */ 18 | camel(str: string): string { 19 | return wrap(str, (result, word, index) => { 20 | return result + word.charAt(0)[index ? 'toUpperCase' : 'toLowerCase']() + word.slice(1) 21 | }) 22 | }, 23 | 24 | /** 25 | * hello-world => HelloWorld 26 | */ 27 | capitalize(str: string): string { 28 | return wrap(str, (result, word, index) => { 29 | return result + word.charAt(0).toUpperCase() + word.slice(1) 30 | }) 31 | }, 32 | 33 | /** 34 | * hello-world => HELLO_WORLD 35 | */ 36 | upper(str: string): string { 37 | return wrap(str, (result, word, index) => { 38 | return result + (index ? '_' : '') + word.toUpperCase() 39 | }) 40 | }, 41 | 42 | /** 43 | * hello-world => hello_world 44 | */ 45 | snake(str: string): string { 46 | return wrap(str, (result, word, index) => { 47 | return result + (index ? '_' : '') + word.toLowerCase() 48 | }) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/file/Template.ts: -------------------------------------------------------------------------------- 1 | import {ICopySource, ICopyFilterResult, ICopiedFiles, IUserTemplate, IData, IRelated, IInject, toArray} from '../common' 2 | import {Source} from './Source' 3 | import {Application} from '../Application' 4 | 5 | export class Template { 6 | app: Application 7 | constructor(public source: Source, public filePath: string, public data: IData, public custom: IUserTemplate) { 8 | this.app = source.app 9 | } 10 | 11 | filter(copySource: ICopySource): boolean | ICopyFilterResult { 12 | if (typeof this.custom.filter === 'function') { 13 | let result = this.app.runUserFunction('template.filter', this.custom.filter, [copySource], this.custom) 14 | if (result == null) return true 15 | return result 16 | } 17 | return true 18 | } 19 | 20 | afterFilter(fromDir: string, toDir: string, result: ICopiedFiles): void { 21 | let {custom} = this 22 | let {afterFilter} = custom 23 | if (typeof afterFilter === 'function') { 24 | this.app.runUserFunction('template.afterFilter', afterFilter, [fromDir, toDir, result, this], custom) 25 | } 26 | } 27 | 28 | getRelatedSources(): IRelated[] { 29 | let {related} = this.custom 30 | if (typeof related === 'function') { 31 | return toArray(this.app.runUserFunction('template.related', related, [this.data, this.source.fileContent])) 32 | } 33 | return [] 34 | } 35 | 36 | getInjects(): IInject[] { 37 | let {inject} = this.custom 38 | if (typeof inject === 'function') { 39 | return toArray(this.app.runUserFunction('template.inject', inject, [this.data, this.source.fileContent])) 40 | } 41 | return [] 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/install-types.ts: -------------------------------------------------------------------------------- 1 | import * as cli from 'mora-scripts/libs/tty/cli' 2 | import * as info from 'mora-scripts/libs/sys/info' 3 | import * as fs from 'fs-extra' 4 | import * as path from 'path' 5 | import {EOL} from 'os' 6 | 7 | const typeDirs = ['common', 'core'] 8 | const fromDir = path.resolve(__dirname, '../out') 9 | const indexContent = `export * from './common/interface'${EOL}` 10 | 11 | cli({ 12 | usage: 'install-dot-template-types ' 13 | }).parse(function(res) { 14 | let toDir = res._[0] 15 | if (!toDir) return this.error('请指定安装目录') 16 | toDir = path.resolve(toDir) 17 | 18 | const types = typeDirs.map(d => path.join(fromDir, d)).reduce((all, dir) => addTypingFile(all, dir), [] as string[]) 19 | 20 | types.forEach(f => copy(f, path.join(toDir, path.relative(fromDir, f)))) 21 | 22 | const indexFile = path.join(toDir, 'index.d.ts') 23 | if (!fs.existsSync(indexFile)) fs.writeFileSync(indexFile, indexContent) 24 | }) 25 | 26 | function addTypingFile(result: string[], filepath: string) { 27 | let stats = fs.statSync(filepath) 28 | if (stats.isFile() && filepath.endsWith('.d.ts')) result.push(filepath) 29 | else if (stats.isDirectory()) fs.readdirSync(filepath).forEach(f => addTypingFile(result, path.join(filepath, f))) 30 | return result 31 | } 32 | 33 | function copy(from: string, to: string) { 34 | fs.ensureDirSync(path.dirname(to)) 35 | 36 | const content = fs.readFileSync(from) 37 | const exists = fs.existsSync(to) 38 | const equals = exists && fs.readFileSync(to).equals(content) 39 | 40 | if (!exists || !equals) { 41 | info((!exists ? '添加 ' : '更新 ') + to) 42 | fs.writeFileSync(to, content) // 奇怪 window 下没有 fs.copyFileSync 函数 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/top-example/dtpl.js.ejs: -------------------------------------------------------------------------------- 1 | // 这是同目录下 dtpl.ts 编译后的文件,如果不熟悉语法,建议修改 dtpl.ts 文件 2 | // 然后用 tsc 把 dtpl.ts 编译到 dtpl.js 3 | 4 | var path = require('path') 5 | module.exports = function(source) { 6 | return { 7 | templates: [ 8 | { 9 | name: 'template/example', 10 | matches: '*-example', 11 | filter: function (target) { 12 | var dir = path.dirname(target.toPath); 13 | if (path.basename(dir) === 'config') { 14 | return { 15 | filePath: path.resolve(dir, '..', '..', target.name) 16 | }; 17 | } 18 | return true; 19 | }, 20 | overwrite: false 21 | }, 22 | { 23 | name: 'template/widget.tsx.dtpl', 24 | matches: '*-example/widget/**/*.tsx' 25 | }, 26 | { 27 | name: 'template/page.tsx.dtpl', 28 | matches: '*-example/page/**/*.tsx', 29 | related: function (data) { 30 | var styleFile = './style/' + data.fileName + '.css'; 31 | return { 32 | relativePath: styleFile, 33 | reference: "require('" + styleFile + "')", 34 | smartInsertStyle: true 35 | }; 36 | } 37 | }, 38 | { 39 | name: 'template/page.css.dtpl', 40 | matches: '*-example/page/**/*.css' 41 | } 42 | ], 43 | globalData: { 44 | projectName: '<%=ref.dirName%>' 45 | } 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/inc/File.ts: -------------------------------------------------------------------------------- 1 | import {Project} from './Project' 2 | import * as fs from 'fs-extra' 3 | export class File { 4 | constructor(public name: string, public pro: Project) {} 5 | 6 | async create(result: boolean, open?: boolean) { 7 | await this.pro.createTemplateFilesAsync(this.name, result, open) 8 | } 9 | 10 | async related(result: boolean) { 11 | await this.pro.createRelatedFilesAsync(this.name, result) 12 | } 13 | 14 | async createDirectories(result: boolean) { 15 | await this.pro.createDirectoriesAsync(this.name, result) 16 | } 17 | 18 | get path() { 19 | return this.pro.fullPath(this.name) 20 | } 21 | 22 | dir() { 23 | fs.ensureDirSync(this.pro.fullPath(this.name)) 24 | } 25 | write(content: string) { 26 | this.pro.writeFile(this.name, content) 27 | } 28 | 29 | async openAsync() { 30 | return await this.pro.editor.openFileAsync(this.pro.fullPath(this.name)) 31 | } 32 | async closeAsync() { 33 | return await this.pro.editor.closeFileAsync(this.pro.fullPath(this.name)) 34 | } 35 | 36 | delete() { 37 | this.pro.deleteFile(this.name) 38 | } 39 | 40 | content() { 41 | return this.pro.getFileContent(this.name) 42 | } 43 | 44 | shouldExists() { 45 | this.pro.fileShouldExists(this.name) 46 | } 47 | shouldNotExists() { 48 | this.pro.fileShouldNotExists(this.name) 49 | } 50 | 51 | shouldBeTextFile() { 52 | this.pro.fileShouldBeTextFile(this.name) 53 | } 54 | 55 | shouldOpened() { 56 | this.pro.fileShouldOpened(this.name) 57 | } 58 | 59 | shouldNotOpened() { 60 | this.pro.fileShouldNotOpened(this.name) 61 | } 62 | 63 | shouldMatch(match: RegExp | string, exact?: boolean) { 64 | this.pro.fileShouldMatch(this.name, match, exact) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/Application.d.ts: -------------------------------------------------------------------------------- 1 | import { Editor } from './Editor'; 2 | import { Render } from './Render'; 3 | import { Source } from './file/'; 4 | export declare class Application { 5 | editor: Editor; 6 | private event; 7 | private cmder; 8 | render: Render; 9 | rootPath: string; 10 | dotTemplateRootPath: string; 11 | constructor(editor: Editor); 12 | createRelatedFiles: (textFile: string) => Promise; 13 | createDirectories: (folders: string[], noInitError?: boolean) => Promise; 14 | createTemplateFiles: (textFile: string[], open: boolean, noInitError?: boolean) => Promise; 15 | undoOrRedo: () => Promise; 16 | runUserFunction(name: string, fn: (...args: any[]) => T, args?: any[], context?: any): T | undefined; 17 | createSource(filePath: string): Source; 18 | /** 19 | * 将 message 中的 %f 用 files 的相对路径来替换 20 | */ 21 | format(message: string, ...files: string[]): string; 22 | debug(message: string, ...files: string[]): void; 23 | info(message: string, ...files: string[]): void; 24 | warning(message: string, ...files: string[]): void; 25 | error(message: string, e?: any): void; 26 | dispose(): void; 27 | emitNewFile: (filePath: string) => void; 28 | emitCreatedFile: (filePath: string, content: string) => void; 29 | emitDeletedFile: (filePath: string, content: string) => void; 30 | emitUpdatedFile: (filePath: string, newContent: string, oldContent: string) => void; 31 | onCreatedFile: (listener: (filePath: string, content: string) => void) => void; 32 | onDeletedFile: (listener: (filePath: string, content: string) => void) => void; 33 | onUpdatedFile: (listener: (filePath: string, newContent: string, oldContent: string) => void) => void; 34 | } 35 | -------------------------------------------------------------------------------- /packages/dot-template-types/common/data.d.ts: -------------------------------------------------------------------------------- 1 | import { IBasicData } from './interface'; 2 | export declare function data(rootPath: string, filePath: string): IBasicData; 3 | export declare const dataExplain: { 4 | rootPath: { 5 | desc: string; 6 | type: string; 7 | }; 8 | npmPath: { 9 | desc: string; 10 | type: string; 11 | }; 12 | date: { 13 | desc: string; 14 | type: string; 15 | }; 16 | time: { 17 | desc: string; 18 | type: string; 19 | }; 20 | datetime: { 21 | desc: string; 22 | type: string; 23 | }; 24 | user: { 25 | desc: string; 26 | type: string; 27 | }; 28 | pkg: { 29 | desc: string; 30 | type: string; 31 | }; 32 | filePath: { 33 | desc: string; 34 | type: string; 35 | }; 36 | relativeFilePath: { 37 | desc: string; 38 | type: string; 39 | }; 40 | fileName: { 41 | desc: string; 42 | type: string; 43 | }; 44 | fileExt: { 45 | desc: string; 46 | type: string; 47 | }; 48 | dirPath: { 49 | desc: string; 50 | type: string; 51 | }; 52 | dirName: { 53 | desc: string; 54 | type: string; 55 | }; 56 | rawModuleName: { 57 | desc: string; 58 | type: string; 59 | }; 60 | moduleName: { 61 | desc: string; 62 | type: string; 63 | }; 64 | ModuleName: { 65 | desc: string; 66 | type: string; 67 | }; 68 | MODULE_NAME: { 69 | desc: string; 70 | type: string; 71 | }; 72 | module_name: { 73 | desc: string; 74 | type: string; 75 | }; 76 | ref: { 77 | desc: string; 78 | type: string; 79 | }; 80 | }; 81 | -------------------------------------------------------------------------------- /packages/dot-template-core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dot-template-core", 3 | "main": "out/index", 4 | "typings": "out/index.d.ts", 5 | "bin": { 6 | "install-dot-template-types": "out/install-types.js" 7 | }, 8 | "description": "create new files according to pre-defined templates", 9 | "version": "0.2.1", 10 | "license": "MIT", 11 | "scripts": { 12 | "prepublish": "npm run build", 13 | "clear": "remove-all out", 14 | "watch": "tsc -watch -p ./", 15 | "api": "api-extractor run --local", 16 | "build": "npm run clear && tsc -p ./ && node out/install-types.js ../dot-template-types", 17 | "test": "nyc _mocha -R dot src/test/core/test*", 18 | "report": "nyc report --reporter html --report-dir out/coverage" 19 | }, 20 | "nyc": { 21 | "require": [ 22 | "ts-node/register" 23 | ], 24 | "extension": [ 25 | ".ts" 26 | ], 27 | "check-coverage": false, 28 | "include": [ 29 | "src/common/*.ts", 30 | "src/core/**/*.ts" 31 | ], 32 | "exclude": [] 33 | }, 34 | "keywords": [ 35 | "dtpl", 36 | "template", 37 | "file", 38 | "variable" 39 | ], 40 | "repository": { 41 | "type": "git", 42 | "url": "https://github.com/qiu8310/dot-template" 43 | }, 44 | "devDependencies": { 45 | "@microsoft/api-extractor": "^7.18.4", 46 | "@types/ejs": "~2.5.1", 47 | "@types/fs-extra": "~5.0.4", 48 | "@types/minimatch": "~3.0.1", 49 | "@types/mocha": "~2.2.42", 50 | "@types/node": "~8.9.5", 51 | "mocha": "~5.0.5", 52 | "nyc": "~11.4.0", 53 | "ts-node": "~7.0.1", 54 | "typescript": "~3.2.2" 55 | }, 56 | "dependencies": { 57 | "ejs": "~2.5.9", 58 | "fs-extra": "~5.0.0", 59 | "ignore": "~3.3.10", 60 | "minimatch": "~3.0.4", 61 | "mora-common": "~1.0.22", 62 | "mora-scripts": "~1.6.42", 63 | "nunjucks": "~3.0.1", 64 | "tslib": "~1.9.3" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/test_emitNewFile.ts: -------------------------------------------------------------------------------- 1 | import {Project, File, delay} from './inc/' 2 | 3 | let pro: Project 4 | 5 | describe('emit', () => { 6 | beforeEach(() => pro = new Project('createDirectories', '.dtpl')) 7 | afterEach(() => pro.destroy()) 8 | 9 | it('text file', async () => { 10 | let f = new File('upper-foo', pro) 11 | f.write('') 12 | pro.app.emitNewFile(f.path) 13 | 14 | await delay(800) 15 | await pro.matchListens(1, [{type: 'updatedFile', args: [f.name, 'UPPER_FOO', '']}]) 16 | }) 17 | 18 | it('two text file', async () => { 19 | let f1 = new File('upper-foo', pro) 20 | f1.write('') 21 | pro.app.emitNewFile(f1.path) 22 | 23 | await delay(10) 24 | 25 | let f2 = new File('upper-bar', pro) 26 | f2.write('') 27 | pro.app.emitNewFile(f2.path) 28 | 29 | 30 | await delay(800) 31 | await pro.matchListens(2, [ 32 | {type: 'updatedFile', args: [f1.name, 'UPPER_FOO', '']}, 33 | {type: 'updatedFile', args: [f2.name, 'UPPER_BAR', '']} 34 | ]) 35 | }) 36 | 37 | it('folder', async () => { 38 | let f = new File('folder-foo', pro) 39 | f.dir() 40 | pro.app.emitNewFile(f.path) 41 | 42 | await delay(800) 43 | await pro.matchListens(1) 44 | }) 45 | 46 | it('two folder', async () => { 47 | let f1 = new File('folder-foo', pro) 48 | f1.dir() 49 | pro.app.emitNewFile(f1.path) 50 | 51 | await delay(10) 52 | 53 | let f2 = new File('folder-bar', pro) 54 | f2.dir() 55 | pro.app.emitNewFile(f2.path) 56 | 57 | await delay(800) 58 | await pro.matchListens(2) 59 | }) 60 | 61 | it('file and folder', async () => { 62 | let f1 = new File('upper-foo', pro) 63 | f1.write('') 64 | pro.app.emitNewFile(f1.path) 65 | 66 | await delay(10) 67 | 68 | let f2 = new File('folder-bar', pro) 69 | f2.dir() 70 | pro.app.emitNewFile(f2.path) 71 | 72 | await delay(800) 73 | await pro.matchListens(2) 74 | }) 75 | }) 76 | 77 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/Editor.d.ts: -------------------------------------------------------------------------------- 1 | import { IMinimatchOptions } from './common'; 2 | import { Application } from './Application'; 3 | export interface IConfiguration { 4 | debug: boolean; 5 | noExampleWhenCreateDtplFolder: boolean; 6 | watchFilesGlobPattern: string; 7 | commandInvalidTimeout: number; 8 | dtplFolderName: string; 9 | minimatchOptions: IMinimatchOptions; 10 | templateExtensions: { 11 | ejs: string; 12 | dtpl: string; 13 | njk: string; 14 | }; 15 | } 16 | export declare abstract class Editor { 17 | rootPath: string; 18 | app: Application; 19 | EOL: string; 20 | configuration: IConfiguration; 21 | constructor(rootPath: string); 22 | /** 23 | * 组件销毁时会被调用 24 | */ 25 | abstract dispose(): void; 26 | /** 27 | * 弹出确认框 28 | */ 29 | abstract confirm(message: string): Promise; 30 | getRelativeFilePath(file: string): string; 31 | /** 32 | * 文件是否是 js 文件 33 | * 34 | * 如果在 vscode 中可以通过判断 languageId 来准确得到 35 | * 36 | * @param {string} file 37 | */ 38 | isJsFileOrTsFile(file: string): boolean; 39 | /** 40 | * 打开文件 41 | */ 42 | openFileAsync(file: string): Promise; 43 | /** 44 | * 关闭文件 45 | */ 46 | closeFileAsync(file: string): Promise; 47 | /** 48 | * 设置文件内容 49 | * 50 | * @param {string} file 51 | * @param {string} content 52 | */ 53 | setFileContentAsync(file: string, content: string): Promise; 54 | /** 55 | * 同步获取文件的内容 56 | * 57 | * @param {string} file 58 | * @returns 59 | * @memberof Editor 60 | */ 61 | getFileContent(file: string): string; 62 | /** 63 | * 判断文件是否打开了 64 | */ 65 | isOpened(file: string): boolean; 66 | debug(message: string): void; 67 | info(message: string): void; 68 | warning(message: string): void; 69 | error(message: string, e?: any): void; 70 | } 71 | -------------------------------------------------------------------------------- /packages/dot-template-cli/src/app/Client.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net' 2 | import * as fs from 'fs-extra' 3 | import {config, Parser, CliEditor, Application, DtplAgent} from './common' 4 | import * as info from 'mora-scripts/libs/sys/info' 5 | import * as error from 'mora-scripts/libs/sys/error' 6 | 7 | export interface IClientOptions { 8 | socketFile?: string 9 | debug?: boolean 10 | rootPath?: string 11 | } 12 | 13 | export class Client extends DtplAgent { 14 | socket?: net.Socket 15 | parser: Parser 16 | 17 | constructor(options: IClientOptions = {}) { 18 | super() 19 | 20 | let { 21 | socketFile = config.socketFile, 22 | rootPath = process.cwd(), 23 | debug = true 24 | } = options 25 | 26 | this.parser = new Parser() 27 | if (fs.existsSync(socketFile)) { 28 | this.socket = net.connect(socketFile) 29 | this.socket.setEncoding('utf8') 30 | info('Connect to Watcher on socket ' + socketFile) 31 | } else { 32 | this.dtpl = new Application(new CliEditor(rootPath, debug)) 33 | } 34 | } 35 | 36 | async createTemplateFiles(files: string[]) { 37 | if (this.socket) { 38 | this.socket.end(this.parser.encode({type: 'createTemplateFiles', data: files})) 39 | } else { 40 | await super.createTemplateFiles(files) 41 | } 42 | } 43 | async createRelatedFiles(file: string) { 44 | if (this.socket) { 45 | this.socket.end(this.parser.encode({type: 'createRelatedFiles', data: file})) 46 | } else { 47 | await super.createRelatedFiles(file) 48 | } 49 | } 50 | async createDirectories(folders: string[]) { 51 | if (this.socket) { 52 | this.socket.end(this.parser.encode({type: 'createDirectories', data: folders})) 53 | } else { 54 | await super.createDirectories(folders) 55 | } 56 | } 57 | async undoOrRedo() { 58 | if (this.socket) { 59 | await this.socket.end(this.parser.encode({type: 'undoOrRedo', data: ''})) 60 | } else { 61 | error('没有启动 Watcher,无法回滚') 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/adapter/TestEditor.ts: -------------------------------------------------------------------------------- 1 | import {Editor} from '../core/Editor' 2 | import * as warn from 'mora-scripts/libs/sys/warn' 3 | import * as error from 'mora-scripts/libs/sys/error' 4 | import * as info from 'mora-scripts/libs/sys/info' 5 | 6 | export class TestEditor extends Editor { 7 | EOL = '\n' 8 | constructor(rootPath: string, public output: boolean) { 9 | super(rootPath) 10 | this.configuration.debug = true 11 | } 12 | 13 | test_debugs: string[] = [] 14 | debug(message: string) { 15 | if (this.output) console.log(message) 16 | this.test_debugs.push(message) 17 | } 18 | 19 | test_warnings: string[] = [] 20 | warning(message: string) { 21 | if (this.output) warn(message) 22 | this.test_warnings.push(message) 23 | } 24 | 25 | test_infos: string[] = [] 26 | info(message: string) { 27 | if (this.output) info(message) 28 | this.test_infos.push(message) 29 | } 30 | 31 | test_errors: string[] = [] 32 | error(message: string, e: Error | any) { 33 | if (this.output) error(message) 34 | this.test_errors.push(message) 35 | } 36 | 37 | returnWhenConfirm: boolean = true 38 | async confirm(message: string): Promise { 39 | return this.returnWhenConfirm 40 | } 41 | 42 | returnWhenSetFileContent: boolean = true 43 | async setFileContentAsync(file: string, content: string): Promise { 44 | if (this.returnWhenSetFileContent) { 45 | return await super.setFileContentAsync(file, content) 46 | } 47 | return this.returnWhenSetFileContent 48 | } 49 | 50 | openedFiles: string[] = [] 51 | async openFileAsync(file: string): Promise { 52 | await super.openFileAsync(file) // 调用一下,触发代码覆盖 53 | if (this.openedFiles.indexOf(file) < 0) this.openedFiles.push(file) 54 | return true 55 | } 56 | 57 | async closeFileAsync(file: string): Promise { 58 | await super.closeFileAsync(file) // 调用一下,触发代码覆盖 59 | this.openedFiles = this.openedFiles.filter(f => f !== file) 60 | return true 61 | } 62 | 63 | isOpened(file: string): boolean { 64 | super.isOpened(file) // 调用一下,触发代码覆盖 65 | return this.openedFiles.indexOf(file) >= 0 66 | } 67 | 68 | dispose() { 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | * This folder contains all of the files necessary for your extension. 5 | * `package.json` - this is the manifest file in which you declare your extension and command. 6 | The sample plugin registers a command and defines its title and command name. With this information 7 | VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | The file exports one function, `activate`, which is called the very first time your extension is 10 | activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 11 | We pass the function containing the implementation of the command as the second parameter to 12 | `registerCommand`. 13 | 14 | ## Get up and running straight away 15 | * Press `F5` to open a new window with your extension loaded. 16 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 17 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 18 | * Find output from your extension in the debug console. 19 | 20 | ## Make changes 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | ## Explore the API 25 | * You can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts`. 26 | 27 | ## Run tests 28 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests`. 29 | * Press `F5` to run the tests in a new window with your extension loaded. 30 | * See the output of the test result in the debug console. 31 | * Make changes to `test/extension.test.ts` or create new test files inside the `test` folder. 32 | * By convention, the test runner will only consider files matching the name pattern `**.test.ts`. 33 | * You can create folders inside the `test` folder to structure your tests any way you want. 34 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/top-example/readme.md: -------------------------------------------------------------------------------- 1 | # Hi there, 欢迎使用 dot-template ! 2 | 3 | > 这是一个简易的教程文档,会手把手带你过一遍 dot-template 基本功能,如果你已经对它很熟悉了,可以忽略此文件! 4 | > 5 | > 访问此文件的线上版本: https://www.zybuluo.com/qiu8310/note/517787 6 | 7 | 8 | ------------- 9 | 10 | 可能你已经发现了,你只是简单的创建了一个 **.dtpl 文件夹**,系统就已经为你创建了一大堆文件在其目录下。对,这就是 dot-template 的功能,**自动为你创建文件,并生成内容**。 11 | 12 | ## 好了,现在我们来一步步的体验一遍 dot-tempate 的基本功能: 13 | 14 | __* 查看此文档需要结合同目录下的 dtpl.ts 文件一起阅读,dtpl.ts 文件是 dot-template 的配置文件__ 15 | 16 | ### 1. 在项目根目录上创建一个 `my-example` 文件夹试试 17 | 18 | 你会发现 my-example 目录下又多了一大批文件,而且最外层 19 | 目录上也多了几个文件: 20 | 21 | - package.json 22 | - tsconfig.json 23 | - webpack.config.js 24 | - .gitignore 25 | 26 | *如果项目中之前就有这些文件,那么原文件会被放到 .backup 目录内* 27 | 28 | 29 | 它是怎么创建的? 30 | 31 | **请查看 `.dtpl/dtpl.ts` 文件中的 `模板一 : 项目模板` 配置部分** 32 | 33 | 34 | ### 2. 运行刚刚创建的项目(什么,刚刚创建了一个项目!) 35 | 36 | - 先用 npm 在项目根目录下安装依赖包 `npm install` 37 | 38 | - 启动服务 `npm run dev` 39 | 40 | - 打开浏览器,访问 http://localhost:8080/ 41 | 42 | **就这么简单,你就创建了一个 react 项目了!!! 不过 dot-template 并没有到此就结束了!** 43 | 44 | 45 | ### 3. 在 `my-example/widge` 目录上新建一个文件 `Head.tsx` 试试 46 | 47 | 你会发现 dot-template 根据模板 `.dtpl/template/widget.tsx.dtpl` 自动为你填充了一些基本的内容,在不修改源码的情况下,你就可以直接使用这个新创建的组件!你也可以修改源模板的内容来适应你自己的项目! 48 | 49 | **请查看 `.dtpl/dtpl.ts` 文件中的 `模板二 : 文件模板` 配置部分** 50 | 51 | 52 | ### 4. 在 `my-example/page` 目录上新建一个文件 `Test.tsx` 试试 53 | 54 | 你会发现和在 widge 目录创建文件一样,没什么太大的区别。但是,别急:先确保 `Test.tsx` 文件在编辑状态下,接着你按下 `cmd+k cmd+p` (window 下是`ctrl+k ctrl+p`)快捷键试试。 55 | 56 | 你会发现系统自动在 `my-example/page/style` 目录下创建了一个同名的 `Test.css` 文件,并且在 `Test.tsx` 文件中插入了对此 css 的引用! 57 | 58 | **请查看 `.dtpl/dtpl.ts` 文件中的 `模板三 : 创建关联文件,并生成其内容` 配置部分** 59 | 60 | 61 | ### 5. 关于渲染模板使用的数据部分,请查看 `.dtpl/dtpl.ts` 文件末尾 62 | 63 | 64 | ## Last but not Least 65 | 66 | 当你对 dot-template 生成的内容,或新生成的文件不满意时,你可以使用快捷键 `cmd+k cmd+u` (window 下是 `ctrl+k ctrl+u`)来撤销,重复再按一次会重新生成,但是,如果 **1分钟** 后没有撤销的话,就无法再撤销了(主要因为怕误操作,而操作者自己确不知,这样可能会导致文件无法恢复) 67 | 68 | 69 | ## 关于 dot-tempate 项目的地址 70 | 71 | * [dot-template github 源码地址](https://github.com/qiu8310/dot-template) 72 | 73 | * [dot-template vscode 插件地址](https://marketplace.visualstudio.com/items?itemName=qiu8310.dot-template-vscode) 74 | 75 | **如果你喜欢的话,欢迎 [给个 Star](https://github.com/qiu8310/dot-template)** 76 | 77 | **如果你有建议的话,欢迎 [提 Issue](https://github.com/qiu8310/dot-template/issues/new)** 78 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/app/AppRelied.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | 3 | import {App} from './App' 4 | import {IData, IBasicData, data, dataExplain} from 'dot-template-core' 5 | import * as DotProp from 'mora-scripts/libs/lang/DotProp' 6 | 7 | 8 | export class AppRelied { 9 | get app(): App | undefined { 10 | let ext = vscode.extensions.getExtension('qiu8310.dot-template-vscode') 11 | if (ext && ext.isActive) { 12 | return ext.exports 13 | } 14 | return 15 | } 16 | 17 | /** 18 | * 支持在 .dtpl 模板文件的最上面加个 sample 字段,使的可以找到它所对应的数据,尤其是自定义数据 19 | * 20 | * 如,在文件最上面加上一行: 21 | * 22 | * /// sample: src/to/example.ts 23 | */ 24 | private getSampleFile(doc: vscode.TextDocument) { 25 | // 加上了自动匹配同名的 dtpl 模板,所以此需求就不急了 26 | return doc.fileName 27 | } 28 | 29 | data(doc: vscode.TextDocument): IData { 30 | let fileName = this.getSampleFile(doc) 31 | 32 | let {app} = this 33 | if (app && app.dtpl) { 34 | let source = app.dtpl.createSource(fileName) 35 | let tpl = source.match(false) 36 | if (tpl) { 37 | return tpl.data 38 | } else { 39 | return data(app.dtpl.rootPath, fileName) 40 | } 41 | } 42 | return data(vscode.workspace.rootPath || process.cwd(), fileName) 43 | } 44 | 45 | doc(key: string, isKeyTop: boolean, data: any): string { 46 | if (isKeyTop && dataExplain.hasOwnProperty(key)) { 47 | return dataExplain[key as keyof IBasicData].desc 48 | } 49 | return '' 50 | } 51 | 52 | detail(key: string, isKeyTop: boolean, data: any): string { 53 | let type = '' 54 | if (isKeyTop && dataExplain.hasOwnProperty(key)) { 55 | type = `type: ${dataExplain[key as keyof IBasicData].type}\n` 56 | } 57 | let example = data.hasOwnProperty(key) ? data[key] : '' 58 | example = example ? '示例值: ' + JSON.stringify(example) : '' 59 | 60 | return type + example 61 | } 62 | 63 | markdown(word: string, data: any): vscode.MarkdownString { 64 | let str = '' 65 | if (word.indexOf('.') < 0 && dataExplain.hasOwnProperty(word)) { 66 | let d = dataExplain[word as keyof IBasicData] 67 | str += `**${d.desc}**\n\n` 68 | str += `type: ${d.type}\n\n` 69 | } 70 | let example = DotProp.get(data, word) 71 | if (example) { 72 | str += '```\nExample: ' + JSON.stringify(example) + ' \n```' 73 | } 74 | return new vscode.MarkdownString(str) 75 | } 76 | 77 | } 78 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/Render.ts: -------------------------------------------------------------------------------- 1 | import * as DotProp from 'mora-scripts/libs/lang/DotProp' 2 | import * as fs from 'fs-extra' 3 | 4 | import {Application} from './Application' 5 | import {IObject} from './common' 6 | 7 | const TPL_VARABLE_REGEXP = /\$([a-zA-Z][\-\w]*)|\$\{([a-zA-Z][\-\w\.]*)\}/g 8 | 9 | export enum Engine { dtpl = 1, ejs, njk } 10 | 11 | export class Render { 12 | constructor(private app: Application) {} 13 | 14 | judgeEngineByFileExtension(file: string): Engine | null { 15 | let es = this.app.editor.configuration.templateExtensions 16 | if (file.endsWith(es.dtpl)) return Engine.dtpl 17 | else if (file.endsWith(es.ejs)) return Engine.ejs 18 | else if (file.endsWith(es.njk)) return Engine.njk 19 | return null 20 | } 21 | 22 | removeFileEngineExtension(file: string): string { 23 | let len = 0 24 | let es = this.app.editor.configuration.templateExtensions 25 | if (file.endsWith(es.ejs)) { 26 | len = es.ejs.length 27 | } else if (file.endsWith(es.dtpl)) { 28 | len = es.dtpl.length 29 | } else if (file.endsWith(es.njk)) { 30 | len = es.njk.length 31 | } 32 | return file.substr(0, file.length - len) 33 | } 34 | 35 | renderFile(file: string, data: IObject): string { 36 | let engine = this.judgeEngineByFileExtension(file) 37 | let content = fs.readFileSync(file).toString() 38 | if (engine == null) return content 39 | return this.renderContent(content, data, engine) 40 | } 41 | 42 | renderContent(content: string, data: IObject, engine: Engine | null): string { 43 | if (engine === Engine.dtpl) return this.renderDtplContent(content, data) 44 | else if (engine === Engine.ejs) return this.renderEjsContent(content, data) 45 | else if (engine === Engine.njk) return this.renderNjkContent(content, data) 46 | else return content 47 | } 48 | 49 | renderDtplContent(content: string, data: IObject): string { 50 | return content.replace(TPL_VARABLE_REGEXP, (raw, key1, key2) => { 51 | let key = key1 || key2 52 | if (key in data) return data[key] 53 | if (key.indexOf('.') > 0 && DotProp.has(data, key)) return DotProp.get(data, key) 54 | return raw 55 | }) 56 | } 57 | 58 | renderEjsContent(content: string, data: IObject): string { 59 | return require('ejs').compile(content, {})(data) 60 | } 61 | 62 | renderNjkContent(content: string, data: IObject): string { 63 | return require('nunjucks').renderString(content, data) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/config/dtpl.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as _ from '../common/interface' 3 | 4 | export default function(source: _.Source): _.IDtplConfig { 5 | const config = source.app.editor.configuration 6 | const interfaceFile = path.resolve(source.app.dotTemplateRootPath, 'out', 'common', 'interface') 7 | .replace(/\\/g, '/') // window 上的路径 \ 需要换成 / 8 | 9 | return { 10 | templates: [ 11 | { 12 | name: '../../res/template/top-no-example/dtpl.ts.ejs', 13 | localData: {ref: {interface: interfaceFile}}, 14 | matches() { 15 | let p = source.filePath 16 | let dirName = path.basename(path.dirname(p)) 17 | let fileName = path.basename(p) 18 | return fileName === 'dtpl.ts' && dirName === config.dtplFolderName 19 | } 20 | }, 21 | { 22 | name: '../../res/template/top-no-example/dtpl.cjs.ejs', 23 | localData: {ref: {interface: interfaceFile}}, 24 | matches() { 25 | let p = source.filePath 26 | let dirName = path.basename(path.dirname(p)) 27 | let fileName = path.basename(p) 28 | return ['dtpl.js', 'dtpl.cjs'].includes(fileName) && dirName === config.dtplFolderName 29 | } 30 | }, 31 | { 32 | // 指定模板名称,需要在同目录下有个同名的文本文件或者文件夹 33 | // 当前指定的是一个文件夹模板 34 | name: '../../res/template', 35 | 36 | // 根据用户当前正在创建或编辑的文件的信息来判断是否需要使用此模板来处理此文件 37 | matches: (minimatch, source) => { 38 | let {isDirectory, filePath} = source 39 | return isDirectory && path.basename(filePath) === config.dtplFolderName 40 | }, 41 | 42 | /** 43 | * 当用户创建了指定的文件夹后,系统会自动复制此模板文件夹下的所有文件到这个新创建的文件 44 | * 你可以用此函数来过滤掉一些你不需要复制的文件 45 | * 或者返回新的文件路径和内容 46 | */ 47 | filter(copy: _.ICopySource): boolean | _.ICopyFilterResult { 48 | let noExample = source.app.editor.configuration.noExampleWhenCreateDtplFolder 49 | let dirName = path.basename(path.dirname(copy.fromPath)) 50 | 51 | if (dirName === 'top-example' || dirName === 'top-no-example') { 52 | if (dirName === 'top-example' && noExample || dirName === 'top-no-example' && !noExample) { 53 | return false 54 | } 55 | return { 56 | name: copy.name, 57 | filePath: path.resolve(copy.toPath, '..', '..', copy.name) 58 | } 59 | } 60 | 61 | if (noExample) return false 62 | return { 63 | name: copy.rawName, 64 | content: copy.rawContent 65 | } 66 | } 67 | } 68 | ], 69 | 70 | globalData: { 71 | interface: interfaceFile 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /packages/dot-template-cli/src/app/common.ts: -------------------------------------------------------------------------------- 1 | import * as os from 'os' 2 | import * as path from 'path' 3 | import * as events from 'events' 4 | import * as findup from 'mora-scripts/libs/fs/findup' 5 | import { StringDecoder, NodeStringDecoder } from 'string_decoder' 6 | import * as error from 'mora-scripts/libs/sys/error' 7 | import {Application} from 'dot-template-core' 8 | 9 | export {Application} from 'dot-template-core' 10 | export {CliEditor} from '../adapter/CliEditor' 11 | 12 | export const config = { 13 | socketFile: getSocketFile(path.join(os.tmpdir(), 'dtpl.socket')) 14 | } 15 | 16 | export function getSocketFile(file: string) { 17 | if (process.platform === 'win32') { 18 | file = file.replace(/^\//, '') 19 | file = file.replace(/\//g, '-') 20 | file = '\\\\.\\pipe\\' + file 21 | } 22 | return file 23 | } 24 | 25 | export type IParserMessageType = 'createTemplateFiles' | 'createRelatedFiles' | 'createDirectories' | 'undoOrRedo' 26 | export interface IParserMessage { 27 | type: IParserMessageType 28 | data: any 29 | } 30 | 31 | export function getRootPath(rootPath?: string | null): string { 32 | if (typeof rootPath === 'string' && rootPath) return path.resolve(rootPath) 33 | try { 34 | return path.dirname(findup.pkg()) 35 | } catch(e) { 36 | return path.resolve(process.cwd()) 37 | } 38 | } 39 | 40 | export class Parser extends events.EventEmitter { 41 | private decoder: NodeStringDecoder 42 | private data: string = '' 43 | 44 | constructor() { 45 | super() 46 | this.decoder = new StringDecoder('utf8') 47 | } 48 | 49 | encode(message: IParserMessage) { 50 | return JSON.stringify(message) + '\n'; 51 | } 52 | 53 | feed(buf: Buffer) { 54 | let { data } = this; 55 | data += this.decoder.write(buf); 56 | let i, start = 0; 57 | while ((i = data.indexOf('\n', start)) >= 0) { 58 | const json = data.slice(start, i); 59 | const message = JSON.parse(json); 60 | this.emit('message', message); 61 | start = i + 1; 62 | } 63 | this.data = data.slice(start); 64 | } 65 | } 66 | 67 | export class DtplAgent { 68 | // @ts-ignore 69 | dtpl: Application 70 | constructor() {} 71 | private async wrap(fn: () => Promise) { 72 | try { 73 | return await fn() 74 | } catch (e) { 75 | error(e) 76 | return false 77 | } 78 | } 79 | 80 | async createTemplateFiles(files: string[]) { 81 | await this.wrap(() => this.dtpl.createTemplateFiles(files, false)) 82 | } 83 | async createRelatedFiles(file: string) { 84 | await this.wrap(() => this.dtpl.createRelatedFiles(file)) 85 | } 86 | async createDirectories(folders: string[]) { 87 | await this.wrap(() => this.dtpl.createDirectories(folders)) 88 | } 89 | async undoOrRedo() { 90 | await this.wrap(() => this.dtpl.undoOrRedo()) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/common/data.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import {transformer} from './transformer' 3 | import {IBasicData} from './interface' 4 | 5 | export function data(rootPath: string, filePath: string): IBasicData { 6 | let d = new Date() 7 | let pad = (n: number): number | string => n < 10 ? '0' + n : n 8 | let date = [d.getFullYear(), d.getMonth() + 1, d.getDate()].map(pad).join('-') 9 | let time = [d.getHours(), d.getMinutes()].map(pad).join(':') 10 | let datetime = date + ' ' + time 11 | 12 | let npmPath = path.join(rootPath, 'node_modules') 13 | 14 | let pkg = {} 15 | try { pkg = require(path.join(rootPath, 'package.json')) } catch (e) { } 16 | 17 | let dirPath = path.dirname(filePath) 18 | let fileExt = path.extname(filePath) 19 | let fileName = path.basename(filePath, fileExt) 20 | let dirName = path.basename(dirPath) 21 | let relativeFilePath = path.relative(rootPath, filePath) 22 | 23 | return { 24 | date, 25 | time, 26 | datetime, 27 | 28 | user: process.env.USER || '', 29 | pkg, 30 | 31 | rootPath, 32 | npmPath, 33 | filePath, 34 | dirPath, 35 | fileName, 36 | dirName, 37 | fileExt, 38 | relativeFilePath, 39 | 40 | rawModuleName: fileName, 41 | moduleName: transformer.camel(fileName), 42 | ModuleName: transformer.capitalize(fileName), 43 | MODULE_NAME: transformer.upper(fileName), 44 | module_name: transformer.snake(fileName) 45 | } 46 | } 47 | 48 | export const dataExplain = { 49 | /*# INJECT_START dataExplain #*/ 50 | rootPath: {desc: '项目根目录的绝对路径', type: 'string'}, 51 | npmPath: {desc: '项目下的 node_modules 目录的绝对路径', type: 'string'}, 52 | date: {desc: '当前日期,格式:yyyy-mm-dd', type: 'string'}, 53 | time: {desc: '当前时间,格式: hh-mm', type: 'string'}, 54 | datetime: {desc: '当前日期和时间,格式:yyyy-mm-dd hh-mm', type: 'string'}, 55 | user: {desc: '当前用户,通过读取环境变量中的 USER 字段而获取到的', type: 'string'}, 56 | pkg: {desc: '当前项目的 package.json 所对应的 JSON 对象', type: '{[key: string]: any}'}, 57 | filePath: {desc: '当前文件的绝对路径', type: 'string'}, 58 | relativeFilePath: {desc: '当前文件相对于根目录的路径', type: 'string'}, 59 | fileName: {desc: '当前文件的名称,不带路径和后缀', type: 'string'}, 60 | fileExt: {desc: '当前文件的后缀名', type: 'string'}, 61 | dirPath: {desc: '当前文件所在的目录的绝对路径', type: 'string'}, 62 | dirName: {desc: '当前文件所在的目录的名称', type: 'string'}, 63 | rawModuleName: {desc: 'fileName 的别名,即当前文件的名称(不含后缀)', type: 'string'}, 64 | moduleName: {desc: '驼峰形式的 fileName', type: 'string'}, 65 | ModuleName: {desc: '单词首字母都大写的形式的 fileName', type: 'string'}, 66 | MODULE_NAME: {desc: '所有字母都大写,中间以下划线连接的 fileName', type: 'string'}, 67 | module_name: {desc: '所有字母都小写,中间以下划线连接的 fileName', type: 'string'}, 68 | ref: {desc: '创建 related 文件时,原文件的 IData 对象;或者创建文件夹模板内的文件时,文件夹的 IData 对象', type: 'IData'} 69 | /*# INJECT_END #*/ 70 | } 71 | -------------------------------------------------------------------------------- /packages/dot-template-types/core/commands/Command.d.ts: -------------------------------------------------------------------------------- 1 | import { Application } from '../Application'; 2 | import { Template } from '../file/Template'; 3 | export interface ICommandInitOptions { 4 | /** 5 | * 指定过期时间,即如过超过这个时间就无法 execute 或 rollback 6 | */ 7 | timeout?: number; 8 | } 9 | /** 10 | * 命令组件有效期过了,无法再执行或回滚 11 | */ 12 | export declare class CommandTimeoutError extends Error { 13 | constructor(command: Command, expiredSeconds: number); 14 | } 15 | export declare enum CommandStatus { 16 | /** 已经完成初始化 */ 17 | INIT = 0, 18 | /** 已经执行过了,没有 rollback, rollback 即回到了 INIT */ 19 | EXECUTED = 1 20 | } 21 | export declare abstract class Command { 22 | name: string; 23 | protected app: Application; 24 | private options; 25 | /** 26 | * 当前命令的状态,是刚初始化还是已经执行过了 27 | */ 28 | status: CommandStatus; 29 | /** 30 | * 标识当前命令是否有效 31 | * 32 | * 在命令初始化时,可能会有些非法参数,这时可以将 Command 设置成 invalid, 33 | * 这样就不会被记录到 Commander 中 34 | * 35 | * @type {boolean} 36 | * @memberof Command 37 | */ 38 | invalid: boolean; 39 | /** 40 | * 上次运行时间 41 | */ 42 | private lastRunTimestamp?; 43 | constructor(name: string, app: Application, options: ICommandInitOptions); 44 | /** 45 | * 输出 debug 信息 46 | */ 47 | protected debug(message: string, ...files: string[]): void; 48 | /** 49 | * 对文本文件或文件夹过滤,需要文件为空 50 | * 51 | * 文本文件为空: 文件不存在,或者文件内容为空 52 | * 文件夹为空: 文件不存在,或者文件夹内无其它文件 53 | * 54 | * @param filePaths 相对于根目录的文件路径 55 | * @param isDirectory 是要过滤文本文件还是目录 56 | */ 57 | protected filter(filePaths: string[], isDirectory: boolean): string[]; 58 | protected inject(tpl: Template): Promise; 59 | protected createFileAsync(file: string, content: string): Promise; 60 | protected setFileContentAsync(file: string, newContent: string, oldContent: string): Promise; 61 | protected unlinkFileAsync(file: string, fileContent: string): Promise; 62 | /** 63 | * 命令是否可运行 64 | * 65 | * @readonly 66 | * @type {boolean} 67 | */ 68 | /** 69 | * 运行命令 70 | * @param {boolean} forward true 表示 execute, false 表示 rollback 71 | */ 72 | run(forward: boolean): Promise; 73 | /** 74 | * 75 | * 执行当前命令 76 | * 77 | * @abstract 78 | * @returns {Promise} 命令是否执行成功 79 | * 80 | * - true:表示执行正常,会将命令打入历史记录 81 | * - false: 表示执行不正常,不会将命令打入历史记录 82 | * 83 | * @throws 抛出异常的话,命令也不会打入历史记录 84 | * 85 | * @memberof Command 86 | */ 87 | protected abstract execute(): Promise; 88 | /** 89 | * 90 | * 回滚当前命令 91 | * 92 | * @abstract 93 | * @returns {Promise} 94 | * 95 | * - true:表示执行正常,会将命令打入历史记录 96 | * - false: 表示执行不正常,不会将命令打入历史记录 97 | * 98 | * @throws 抛出异常的话,命令也不会打入历史记录 99 | * 100 | * @memberof Command 101 | */ 102 | protected abstract rollback(): Promise; 103 | } 104 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/Editor.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | import * as os from 'os' 4 | import * as error from 'mora-scripts/libs/sys/error' 5 | import * as warn from 'mora-scripts/libs/sys/warn' 6 | import * as info from 'mora-scripts/libs/sys/info' 7 | import {IMinimatchOptions} from './common' 8 | import {Application} from './Application' 9 | 10 | export interface IConfiguration { 11 | debug: boolean 12 | noExampleWhenCreateDtplFolder: boolean 13 | watchFilesGlobPattern: string 14 | commandInvalidTimeout: number 15 | dtplFolderName: string 16 | minimatchOptions: IMinimatchOptions 17 | templateExtensions: { 18 | ejs: string 19 | dtpl: string 20 | njk: string 21 | } 22 | } 23 | 24 | export abstract class Editor { 25 | // @ts-ignore 26 | app: Application 27 | EOL: string = os.EOL 28 | configuration: IConfiguration = { 29 | debug: false, 30 | noExampleWhenCreateDtplFolder: false, 31 | watchFilesGlobPattern: '**/*', 32 | commandInvalidTimeout: 60000, 33 | dtplFolderName: '.dtpl', 34 | minimatchOptions: { 35 | matchBase: true, 36 | nocomment: true, 37 | dot: true 38 | }, 39 | templateExtensions: { 40 | ejs: '.ejs', 41 | dtpl: '.dtpl', 42 | njk: '.njk' 43 | } 44 | } 45 | 46 | constructor(public rootPath: string) {} 47 | 48 | /** 49 | * 组件销毁时会被调用 50 | */ 51 | abstract dispose(): void 52 | 53 | /** 54 | * 弹出确认框 55 | */ 56 | abstract confirm(message: string): Promise 57 | 58 | getRelativeFilePath(file: string) { 59 | return path.relative(this.rootPath, file) 60 | } 61 | 62 | /** 63 | * 文件是否是 js 文件 64 | * 65 | * 如果在 vscode 中可以通过判断 languageId 来准确得到 66 | * 67 | * @param {string} file 68 | */ 69 | isJsFileOrTsFile(file: string): boolean { 70 | return /\.[jt]sx?$/i.test(file) 71 | } 72 | 73 | /** 74 | * 打开文件 75 | */ 76 | async openFileAsync(file: string): Promise { 77 | return true 78 | } 79 | 80 | /** 81 | * 关闭文件 82 | */ 83 | async closeFileAsync(file: string): Promise { 84 | return true 85 | } 86 | 87 | /** 88 | * 设置文件内容 89 | * 90 | * @param {string} file 91 | * @param {string} content 92 | */ 93 | async setFileContentAsync(file: string, content: string): Promise { 94 | fs.writeFileSync(file, content) 95 | return true 96 | } 97 | 98 | /** 99 | * 同步获取文件的内容 100 | * 101 | * @param {string} file 102 | * @returns 103 | * @memberof Editor 104 | */ 105 | getFileContent(file: string) { 106 | return fs.readFileSync(file).toString() 107 | } 108 | 109 | /** 110 | * 判断文件是否打开了 111 | */ 112 | isOpened(file: string): boolean { 113 | return false 114 | } 115 | 116 | /* istanbul ignore next */ 117 | debug(message: string) { 118 | if (this.configuration.debug) console.log('[dtpl] ' + message) 119 | } 120 | /* istanbul ignore next */ 121 | info(message: string) { 122 | info('[dtpl] ' + message) 123 | } 124 | /* istanbul ignore next */ 125 | warning(message: string) { 126 | warn('[dtpl] ' + message) 127 | } 128 | /* istanbul ignore next */ 129 | error(message: string, e?: any) { 130 | error(message) 131 | if (e) console.error(e) 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/common/helper.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as fs from 'fs-extra' 3 | 4 | const ignore = require('ignore') 5 | 6 | /** 7 | * 按先后顺序一个个用 run 函数来运行 tasks 中的字段 8 | * 9 | * @export 10 | * @template T 11 | * @template R 12 | * @param {T[]} tasks 要运行的任务 13 | * @param {(task: T) => Promise} run 运行函数 14 | * @returns {Promise} 返回每个 tasks 对应的结果组成的数组 15 | */ 16 | export async function series(tasks: T[], run: (task: T, index: number, tasks: T[]) => Promise): Promise { 17 | let result: R[] = [] 18 | if (!tasks.length) return result 19 | 20 | let handle: any = tasks.slice(1).reduce( 21 | (prev, task: T, index, ref) => { 22 | return async () => { 23 | result.push(await prev()) 24 | return await run(task, index + 1, ref) 25 | } 26 | }, 27 | async () => await run(tasks[0], 0, tasks) 28 | ) 29 | result.push(await handle()) 30 | return result 31 | } 32 | 33 | /** 34 | * 对数组去重 35 | */ 36 | export function unique(items: T[], uniqueKey?: K) { 37 | return items.reduce((result: T[], item) => { 38 | if (uniqueKey) { 39 | if (result.every(_ => _[uniqueKey] !== item[uniqueKey])) result.push(item) 40 | } else { 41 | if (result.indexOf(item) < 0) result.push(item) 42 | } 43 | return result 44 | }, []) 45 | } 46 | 47 | 48 | const importOrExportRegexp = /^\s*(?:import|export)\s+.*?\s+from\s+['"]([^'"]+)['"]/mg 49 | const requireRegExp = /^\s*(?:var|let|const|import)\s+\w+\s+=\s+require\(['"]([^'"]+)['"]\)/mg 50 | 51 | /** 52 | * 查找 js 文件中引用的其它文件,一般是通过 require 或 import 语法来引用的 53 | * 54 | * 如: 55 | * 56 | * ``` 57 | * import Test from './Test' 58 | * export * from './Test' 59 | * const Test = require('./Test') 60 | * import Test = require('./Test') 61 | * ``` 62 | */ 63 | export function findJsRelatedFiles(jsfile: string, fileContent: string): string[] { 64 | let result: string[] = [] 65 | 66 | let add = (from: string): string => { 67 | // 一定要是相对目录 68 | if (from[0] === '.') { 69 | let file = path.resolve(path.dirname(jsfile), from) 70 | 71 | // 如果没有后缀,要加上当前文件的后缀 72 | // TODO: 有可能是文件夹 73 | if (!(/\.\w+$/.test(file))) file += path.extname(jsfile) 74 | 75 | if (result.indexOf(file) < 0) result.push(file) 76 | } 77 | return '' 78 | } 79 | 80 | fileContent.replace(importOrExportRegexp, (raw, from) => add(from)) 81 | fileContent.replace(requireRegExp, (raw, from) => add(from)) 82 | 83 | return result 84 | } 85 | 86 | const fileMTimeCache: {[key: string]: number} = {} 87 | export function requireFile(file: string): any { 88 | let mtime = fs.statSync(file).mtime.getTime() 89 | if (!fileMTimeCache[file] || fileMTimeCache[file] !== mtime) { 90 | delete require.cache[require.resolve(file)] 91 | } 92 | fileMTimeCache[file] = mtime 93 | return require(file) 94 | } 95 | 96 | export function getIgnore(rootPath: string): any { 97 | let file = path.join(rootPath, '.gitignore') 98 | let ig = ignore().add(['node_modules/**', '.git/**', '.vscode/**']) 99 | 100 | try { 101 | ig.add(fs.readFileSync(file).toString()) 102 | } catch (e) {} 103 | 104 | return ig 105 | } 106 | 107 | export function toArray(item: undefined | T | T[]): T[] { 108 | if (!item) return [] 109 | return Array.isArray(item) ? item : [item] 110 | } 111 | -------------------------------------------------------------------------------- /packages/dot-template-cli/src/app/Watcher.ts: -------------------------------------------------------------------------------- 1 | import * as net from 'net' 2 | import * as fs from 'fs-extra' 3 | import * as chokidar from 'chokidar' 4 | import * as error from 'mora-scripts/libs/sys/error' 5 | import * as info from 'mora-scripts/libs/sys/info' 6 | 7 | import {Application, CliEditor, config, Parser, IParserMessage, DtplAgent} from './common' 8 | 9 | export interface IWatcherOptions { 10 | socketFile?: string 11 | debug?: boolean 12 | rootPath?: string 13 | watch?: boolean 14 | watchGlobPatterns?: string[] 15 | } 16 | 17 | 18 | export class Watcher extends DtplAgent { 19 | server: net.Server | null 20 | private socketFile: string 21 | private parser: Parser 22 | private fswatcher?: chokidar.FSWatcher 23 | 24 | destroy = () => { 25 | if (fs.existsSync(this.socketFile)) { 26 | fs.unlinkSync(this.socketFile) 27 | } 28 | if (this.fswatcher) { 29 | this.fswatcher.close() 30 | this.fswatcher = undefined 31 | } 32 | if (this.server) { 33 | this.server.close() 34 | this.server = null 35 | } 36 | } 37 | 38 | constructor(options: IWatcherOptions = {}) { 39 | super() 40 | 41 | let { 42 | socketFile = config.socketFile, 43 | rootPath = process.cwd(), 44 | watch = false, 45 | watchGlobPatterns = [], 46 | debug = false 47 | } = options 48 | this.socketFile = socketFile 49 | this.dtpl = new Application(new CliEditor(rootPath, debug)) 50 | 51 | this.parser = new Parser() 52 | this.parser.on('message', (json: IParserMessage) => this.handleMessage(json)) 53 | 54 | this.server = net.createServer(this.handleConnection.bind(this)) 55 | this.server.on('error', error) 56 | 57 | if (watch) { 58 | this.watchDir(watchGlobPatterns.length ? watchGlobPatterns : rootPath) 59 | } 60 | process.on('exit', this.destroy) 61 | process.on('SIGINT', this.destroy) 62 | process.on('SIGHUP', this.destroy) 63 | } 64 | 65 | watchDir(dir: string | string[]) { 66 | let emit = (filepath: string) => { 67 | if (typeof filepath === 'string') this.dtpl.emitNewFile(filepath) 68 | } 69 | 70 | this.fswatcher = chokidar.watch(dir, { 71 | ignored: ['**/node_modules/**', '**/.git/**'], 72 | ignoreInitial: true 73 | }).on('add', emit).on('addDir', emit) 74 | } 75 | 76 | handleConnection(socket: net.Socket) { 77 | info('receive new connection') 78 | socket.on('data', (buf) => this.parser.feed(buf)) 79 | socket.on('close', () => info('connection closed')) 80 | } 81 | 82 | handleMessage(m: IParserMessage) { 83 | let {type, data} = m 84 | info(`receive command ${type} ${JSON.stringify(data)}`) 85 | if (type === 'createTemplateFiles') { 86 | this.createTemplateFiles(data) 87 | } else if (type === 'createDirectories') { 88 | this.createDirectories(data) 89 | } else if (type === 'createRelatedFiles') { 90 | this.createRelatedFiles(data) 91 | } else { 92 | this.undoOrRedo() 93 | } 94 | } 95 | 96 | listen(callback?: () => void) { 97 | let {socketFile} = this 98 | if (fs.existsSync(socketFile)) { 99 | throw new Error(`Socket file ${socketFile} already exists`) 100 | } 101 | 102 | if (this.server) { 103 | this.server.listen(socketFile, () => { 104 | if (callback) callback() 105 | info(`Watcher listen on ${socketFile}`) 106 | }) 107 | } else { 108 | throw new Error('Already destroied!') 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /packages/dot-template-cli/src/cli.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as cli from 'mora-scripts/libs/tty/cli' 4 | import * as path from 'path' 5 | import {Watcher, IWatcherOptions, Client, IClientOptions, getRootPath} from './app/' 6 | 7 | const commonOpts = { 8 | 'r | rootPath': ' 指定项目根目录,没有指定则使用 package.json 文件所在目录或者当前目录', 9 | 'd | debug': ' 调试模式(无 watch 时才有效)', 10 | 's | socket': ' 指定 socket 文件路径' 11 | } 12 | 13 | const commands = { 14 | touch: { 15 | desc: '新建文本文件', 16 | cmd: touch 17 | }, 18 | mkdir: { 19 | desc: '新建文件夹', 20 | cmd: mkdir 21 | }, 22 | related: { 23 | desc: '创建指定的文件的关联文件', 24 | cmd: related 25 | }, 26 | revoke: { 27 | desc: '撤销或重做上一次命令', 28 | cmd: revoke 29 | }, 30 | watch: { 31 | desc: '启动 watch 服务器', 32 | cmd: watch 33 | } 34 | } 35 | 36 | cli({ 37 | usage: 'dtpl [options] ' 38 | }) 39 | .commands({ 40 | ...commands 41 | }) 42 | .parse(function() { 43 | return this.help() 44 | }) 45 | 46 | async function client(r: any, fn: (c: Client) => Promise) { 47 | let opt: IClientOptions = { 48 | rootPath: getRootPath(r.rootPath), 49 | debug: !!r.debug 50 | } 51 | if (r.socket) opt.socketFile = path.resolve(r.socket) 52 | let c = new Client(opt) 53 | await fn(c) 54 | } 55 | 56 | function resolve(files: string[]) { 57 | return files.map(f => path.resolve(f)) 58 | } 59 | 60 | function touch(topRes: any) { 61 | cli({ 62 | usage: 'dtpl touch ', 63 | desc: '新建文本文件,并尝试查找模板,有合适的模板的话,则渲染模板内容到此文件上', 64 | version: false 65 | }) 66 | .options(commonOpts) 67 | .parse(topRes._, function(res) { 68 | if (res._.length) { 69 | client(res, c => c.createTemplateFiles(resolve(res._))) 70 | } else { 71 | this.error('请指定要创建的文件') 72 | } 73 | }) 74 | } 75 | 76 | function related(topRes: any) { 77 | cli({ 78 | usage: 'dtpl related ', 79 | desc: '新建文本文件,并尝试查找模板,有合适的模板的话,则渲染模板内容到此文件上', 80 | version: false 81 | }) 82 | .options(commonOpts) 83 | .parse(topRes._, function(res) { 84 | if (res._.length === 1) { 85 | client(res, c => c.createRelatedFiles(resolve(res._)[0])) 86 | } else { 87 | this.error('必须只能指定一个文件') 88 | } 89 | }) 90 | } 91 | 92 | function mkdir(topRes: any) { 93 | cli({ 94 | usage: 'dtpl mkdir ', 95 | desc: '新建文件夹,并尝试查找模板,有合适的模板的话,则复制模板内的所有文件到此文件夹内', 96 | version: false 97 | }) 98 | .options(commonOpts) 99 | .parse(topRes._, function(res) { 100 | if (res._.length) { 101 | client(res, c => c.createDirectories(resolve(res._))) 102 | } else { 103 | this.error('请指定要创建的文件夹') 104 | } 105 | }) 106 | } 107 | 108 | function revoke(topRes: any) { 109 | cli({ 110 | usage: 'dtpl revoke', 111 | desc: '撤销或重做上一次命令', 112 | version: false 113 | }) 114 | .parse(topRes._, function(res) { 115 | client(res, c => c.undoOrRedo()) 116 | }) 117 | } 118 | 119 | function watch(topRes: any) { 120 | cli({ 121 | usage: 'dtpl watch [options]', 122 | desc: [ 123 | '启动 watch 服务器,通过纯一的服务器来处理其它命令', 124 | '这样就可以回滚某个命令,同时可以支持自动监听文件变化' 125 | ], 126 | version: false 127 | }).options({ 128 | ...commonOpts, 129 | 'w | watchFileGlobs': ' 指定要监听的文件,没有指定则会监听根目录' 130 | }).parse(topRes._, function(r) { 131 | let opt: IWatcherOptions = { 132 | rootPath: path.resolve(r.rootPath || process.cwd()), 133 | watch: true, 134 | debug: !!r.debug, 135 | watchGlobPatterns: r.watchFileGlobs ? r.watchFileGlobs : [] 136 | } 137 | if (r.socket) opt.socketFile = path.resolve(r.socket) 138 | new Watcher(opt).listen() 139 | }) 140 | } 141 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/commands/CreateTemplateFilesCommand.ts: -------------------------------------------------------------------------------- 1 | import {Command, ICommandInitOptions} from './Command' 2 | import {Application} from '../Application' 3 | import {series} from '../common' 4 | import * as fs from 'fs-extra' 5 | 6 | /** 7 | * 创建文件并注入模板的命令 8 | * 9 | * **注意:** 10 | * 11 | * - 文件之前是不存在的 12 | * - 可以指定多个文件 13 | * 14 | */ 15 | export class CreateTemplateFilesCommand extends Command { 16 | files: string[] 17 | 18 | /** 19 | * 对应文件的创建前的信和,在撤消命令时,要判断内容是否改过了,改过了要弹出确认框 20 | */ 21 | private infos: Array<{opened: boolean, exists: boolean, newContent: string, content: string}> = [] 22 | 23 | /** 24 | * @param {string[]} files 所有要创建的文件绝对路径,一定要确保文件不存在 25 | * @param {boolean} [open] 是否要打开这些创建好的文件 26 | * @param {boolean} [noInitError] 初始化时不要报错,主要 emitNewFile 时可能是因为用户修改了文件夹的名称 27 | * @memberof CreateFilesCommand 28 | */ 29 | constructor(files: string[], private open: boolean, noInitError: boolean, app: Application, options: ICommandInitOptions) { 30 | super('CreateTemplateFilesCommand', app, options) 31 | 32 | this.files = this.filter(files, false) 33 | 34 | if (!this.files.length) { 35 | this.invalid = true 36 | if (!noInitError) this.app.error('无任何可创建的有效文件:文件路径需要在项目内,并且文件需要不存在,或文件内容为空') 37 | } 38 | } 39 | 40 | async execute(): Promise { 41 | const {app} = this 42 | let {render, editor} = app 43 | let result = await series(this.files, async (file) => { 44 | this.debug('开始处理文件 %f', file) 45 | let src = this.app.createSource(file) 46 | let tpl = src.match(false) 47 | 48 | let opened = false 49 | let content: string = '' 50 | let newContent = tpl ? render.renderFile(tpl.filePath, tpl.data) : '' 51 | 52 | if (tpl) { 53 | this.debug(`渲染文件 %f`, tpl.filePath) 54 | await this.inject(tpl) 55 | } 56 | 57 | let exists = fs.existsSync(file) 58 | 59 | if (exists) { 60 | content = editor.getFileContent(file) 61 | opened = editor.isOpened(file) 62 | } else { 63 | await this.createFileAsync(file, '') 64 | } 65 | 66 | this.infos.push({content, exists, opened, newContent}) 67 | 68 | await this.setFileContentAsync(file, newContent, content) 69 | 70 | if (this.open && !opened) await editor.openFileAsync(file) // 文件打开失败不算错误 71 | this.debug('处理文件 %f 成功', file) 72 | return true 73 | }) 74 | 75 | return result.every(r => r) 76 | } 77 | 78 | async rollback(): Promise { 79 | const {app} = this 80 | let {editor} = app 81 | 82 | let updates: string[] = [] 83 | this.files.forEach((file, i) => { 84 | let info = this.infos[i] 85 | if (info.exists && fs.existsSync(file)) { // 过去和现在都存在 86 | if (editor.getFileContent(file) !== info.newContent) { 87 | updates.push(editor.getRelativeFilePath(file)) 88 | } 89 | } 90 | }) 91 | if (updates.length && false === await editor.confirm(`文件 ${updates.join(', ')} 更新过了,确认要取消创建这些文件吗?`)) { 92 | return false 93 | } 94 | 95 | let result = await series(this.files, async (file, i) => { 96 | this.debug('开始回滚文件 %f' + file) 97 | let info = this.infos[i] 98 | 99 | if (fs.existsSync(file)) { 100 | let currentContent = editor.getFileContent(file) 101 | if (info.exists) { 102 | await this.setFileContentAsync(file, info.content, currentContent) 103 | } else { 104 | await this.unlinkFileAsync(file, currentContent) 105 | } 106 | } else { 107 | if (info.exists) { 108 | await this.createFileAsync(file, info.content) 109 | } 110 | } 111 | 112 | if (info.exists && info.opened && !editor.isOpened(file)) await editor.openFileAsync(file) 113 | this.debug('回滚文件 %f 成功', file) 114 | return true 115 | }) 116 | 117 | return result.every(r => r) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/app/App.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import * as path from 'path' 3 | // import * as fs from 'fs-extra' 4 | // import {findJsRelatedFiles} from '../../common/helper' 5 | import {Application} from 'dot-template-core' 6 | import {VscodeEditor} from '../adapter/VscodeEditor' 7 | 8 | export class App { 9 | private fileSystemWatcher?: vscode.FileSystemWatcher 10 | public dtpl?: Application 11 | 12 | get activeEditor(): vscode.TextEditor | undefined { 13 | return vscode.window.activeTextEditor 14 | } 15 | 16 | constructor() { 17 | vscode.workspace.onDidChangeWorkspaceFolders(e => { 18 | this.dispose() 19 | this.init() 20 | }) 21 | this.init() 22 | } 23 | 24 | init() { 25 | let {workspaceFolders} = vscode.workspace 26 | if (this.dtpl || !workspaceFolders || !workspaceFolders.length) return 27 | 28 | let dtpl = this.dtpl = new Application(new VscodeEditor()) 29 | 30 | this.fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*', false, true, true) 31 | this.fileSystemWatcher.onDidCreate(uri => dtpl.emitNewFile(uri.fsPath)) 32 | 33 | let r = (file: string) => path.relative(dtpl.rootPath, file) 34 | 35 | let sid: NodeJS.Timer 36 | let showMessage = (msg: string) => { 37 | clearTimeout(sid) 38 | let res = vscode.window.setStatusBarMessage(msg, 3000) 39 | sid = setTimeout(() => res.dispose(), 3000) 40 | } 41 | dtpl.onCreatedFile(file => showMessage(`文件 ${r(file)} 创建成功`)) 42 | dtpl.onUpdatedFile(file => showMessage(`文件 ${r(file)} 更新成功`)) 43 | dtpl.onDeletedFile(file => showMessage(`文件 ${r(file)} 删除成功`)) 44 | } 45 | 46 | undoOrRedo = async () => { 47 | if (this.dtpl) await this.dtpl.undoOrRedo() 48 | } 49 | 50 | createTemplateFiles = async () => { 51 | const {dtpl, activeEditor} = this 52 | if (!dtpl) return 53 | if (!activeEditor) { 54 | // 没有打开任何窗口就提示用户创建文件 55 | const input = await vscode.window.showInputBox({placeHolder: '请输入要创建的文件名(相对于根目录,多个文件用";"分隔)'}) 56 | if (input) await dtpl.createTemplateFiles(input.trim().split(/\s*;\s*/).map(f => path.resolve(dtpl.rootPath, f)), true) 57 | } else { 58 | let currentfile = activeEditor.document.fileName 59 | let content = activeEditor.document.getText() 60 | 61 | if (content.trim()) { 62 | // 开个小灶 63 | // js 文件可以自动检查引用了哪些不存在的文件,然后创建它们 64 | // if (dtpl.editor.isJsFileOrTsFile(currentfile)) { 65 | // let notExistFiiles = findJsRelatedFiles(currentfile, content).filter(f => !fs.existsSync(f)) 66 | // if (notExistFiiles.length && await dtpl.createTemplateFiles(notExistFiiles, true)) { 67 | // return // 创建成功就不往下执行了 68 | // } 69 | // } 70 | 71 | // 尝试创建关联文件 72 | if (await dtpl.createRelatedFiles(currentfile)) return 73 | 74 | // 创建新文件 75 | const input = await vscode.window.showInputBox({placeHolder: '请输入要创建的文件名(相对当前文件的路径,多个文件用";"分隔)'}) 76 | if (input) await dtpl.createTemplateFiles(input.trim().split(/\s*;\s*/).map(f => path.resolve(currentfile, '..', f)), true) 77 | 78 | } else { 79 | // 当前打开的文件如果没有内容,则注入模板(如果有的话) 80 | await dtpl.createTemplateFiles([currentfile], false) 81 | } 82 | } 83 | } 84 | 85 | createRelatedFiles = async () => { 86 | const {activeEditor, dtpl} = this 87 | if (!dtpl) return 88 | if (!activeEditor) { 89 | dtpl.warning('当前没有打开的文件,无法创建关联文件') 90 | } else { 91 | await dtpl.createRelatedFiles(activeEditor.document.fileName) 92 | } 93 | } 94 | 95 | // TODO: 需要找到 vscode 中添加右键菜单的功能 96 | // createTemplateFolders = async () => {} 97 | 98 | dispose() { 99 | if (this.fileSystemWatcher) { 100 | this.fileSystemWatcher.dispose() 101 | this.fileSystemWatcher = undefined 102 | } 103 | if (this.dtpl) { 104 | this.dtpl.dispose() 105 | this.dtpl = undefined 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/Commander.ts: -------------------------------------------------------------------------------- 1 | import {Command, ICommandInitOptions} from './commands/Command' 2 | import {Application} from './Application' 3 | import {CreateDirectoriesCommand, CreateRelatedFilesCommand, CreateTemplateFilesCommand} from './commands' 4 | 5 | export class Commander { 6 | history: Command[] = [] 7 | 8 | /** 9 | * 收集所有下次要运行的命令 10 | * 11 | * 系统需要保存命令一个一个的运行 12 | */ 13 | private queue: Array<{command: Command, forward: boolean, handler: (err: any, result: boolean) => void}> = [] 14 | private isRunning: boolean = false 15 | private isChecking: boolean = false 16 | private lastRunningTime?: number 17 | 18 | /** 19 | * 指向 prev 操作要执行的命令的索引 20 | */ 21 | cursor: number = -1 22 | constructor(public app: Application, public length: number) {} 23 | 24 | private async runCommand(command: Command, forward: boolean): Promise { 25 | if (this.isRunning || this.isChecking) { 26 | return await new Promise((resolve, reject) => { 27 | this.queue.push({command, forward, handler(err: any, result: boolean) { 28 | if (err) reject(err) 29 | else resolve(result) 30 | }}) 31 | }) 32 | } 33 | 34 | try { 35 | this.isRunning = true 36 | let result = await command.run(forward) 37 | this.lastRunningTime = Date.now() 38 | this.isRunning = false 39 | 40 | // 检查下一条命令 41 | this.isChecking = true 42 | setTimeout(async () => { 43 | let next = this.queue.shift() 44 | this.isChecking = false 45 | if (next) { 46 | try { 47 | next.handler(null, await this.runCommand(next.command, next.forward)) 48 | } catch (e) { 49 | next.handler(e, false) 50 | } 51 | } 52 | }, 1) 53 | 54 | return result 55 | } catch (e) { 56 | this.isRunning = false 57 | if (e instanceof Error) { 58 | e.message = `执行命令 ${command.name} 出错: ${e.message}` 59 | } 60 | throw e 61 | } 62 | } 63 | 64 | fileMaybeCreatedByCommand() { 65 | return this.isChecking || this.isRunning || (this.lastRunningTime && Date.now() - this.lastRunningTime < 500) 66 | } 67 | 68 | private async add(command: Command): Promise { 69 | if (!command.invalid && await this.runCommand(command, true)) { 70 | this.history.push(command) 71 | if (this.history.length > this.length) { 72 | this.history.shift() // 将第一个命令删除掉 73 | } else { 74 | this.cursor++ 75 | } 76 | return true 77 | } 78 | return false 79 | } 80 | 81 | private getCommonComamndOpts(): ICommandInitOptions { 82 | return {timeout: this.app.editor.configuration.commandInvalidTimeout} 83 | } 84 | 85 | private async wrap(fn: () => Promise): Promise { 86 | try { 87 | return await fn() 88 | } catch (e) { 89 | this.app.error(e.message, e) 90 | return false 91 | } 92 | } 93 | 94 | async addCreateDirectoriesCommand(folders: string[], noInitError: boolean): Promise { 95 | return this.wrap(() => this.add(new CreateDirectoriesCommand(folders, noInitError, this.app, this.getCommonComamndOpts()))) 96 | } 97 | async addCreateRelatedFilesCommand(textFile: string): Promise { 98 | return this.wrap(() => this.add(new CreateRelatedFilesCommand(textFile, this.app, this.getCommonComamndOpts()))) 99 | } 100 | async addCreateTemplateFilesCommand(textFiles: string[], open: boolean, noInitError: boolean): Promise { 101 | return this.wrap(() => this.add(new CreateTemplateFilesCommand(textFiles, open, noInitError, this.app, this.getCommonComamndOpts()))) 102 | } 103 | 104 | get hasNext() { return this.cursor < this.length - 1 } 105 | get hasPrev() { return this.cursor > -1 } 106 | 107 | /** 108 | * 执行历史记录中的下一条命令 109 | * 110 | * @returns {Promise} 如果没有下一条或者命令执行失败,返回 false 111 | * @memberof Commander 112 | */ 113 | async next(): Promise { 114 | if (!this.hasNext) return false 115 | let command = this.history[this.cursor + 1] 116 | 117 | if (true === (await this.runCommand(command, true))) { 118 | this.cursor++ 119 | return true 120 | } 121 | 122 | return false 123 | } 124 | 125 | /** 126 | * 执行历史记录中的上一条命令 127 | * 128 | * @returns {Promise} 如果没有上一条或者命令执行失败,返回 false 129 | * @memberof Commander 130 | */ 131 | async prev(): Promise { 132 | if (!this.hasPrev) return false 133 | let command = this.history[this.cursor] 134 | 135 | if (true === (await this.runCommand(command, false))) { 136 | this.cursor-- 137 | return true 138 | } 139 | 140 | return false 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /packages/dot-template-core/res/template/top-example/dtpl.ts.ejs: -------------------------------------------------------------------------------- 1 | // local interface file: <%=ref.interface%> 2 | import * as _ from 'dot-template-types' 3 | import * as path from 'path' 4 | 5 | /* 6 | 注意: 7 | 8 | 1. 虽然 ts 文件有很强的语法提示,但默认情况下 dot-template 并不支持, 9 | 你需要把它编译成 js 文件并放在同目录下;不过 dot-template 也是可 10 | 以支持处理 ts 文件的,需要你在当前项目中安装 `ts-node` 和 11 | `typescript` 组件。 12 | 13 | 2. 在此脚本中使用 console 语句是不会输出在控制面板的,因为此脚本是在 14 | vscode 插件中执行的,插件的输出不在当前环境中;不过你可以设置配置 15 | 项中的 dot-template-vscode.debug 为 true,并在此程序中执行: 16 | 17 | source.app.debug('...') 18 | 19 | 3. 当 matches 是字符串时,可以只匹配 basename,但如果 matches 带 20 | 路径时,就要从项目根路径开始匹配,否则无法匹配成功。(主要是是因为 21 | minimatch 的选项 matchBase 设置为 true 了,你可以用 22 | dot-template-vscode.minimatchOptions 来修改默认的配置) 23 | */ 24 | 25 | export default function(source: _.Source): _.IDtplConfig { 26 | return { 27 | templates: [ 28 | 29 | /** 30 | * ================= 模板一 : 项目模板 ================= 31 | * 当新建一个以 "-example" 结尾的文件夹时,会自 32 | * 动将 .dtpl/template/example 下的所有文件内 33 | * 容复制到你新建的文件夹内 34 | * 35 | * 另外,由于以 .dtpl 结尾的文件是模板文件,所以 36 | * 系统会用内部生成的数据和用户指定的数据去渲染模板, 37 | * 生成新的内容,然后去除模板文件的后缀,写入新的对应 38 | * 的文件内 39 | * 40 | * 支持的模板有: dtpl, ejs 和 nunjunk;对应的后缀名分别默认为: .dtpl, .ejs, .njk 41 | */ 42 | { 43 | name: 'template/example', // 指定当前目录下的模板名称 44 | matches: '*-example', // 匹配新建的文件夹的路径(使用了 minimatch 来匹配) 45 | 46 | // 过滤模板内的文件,主要功能有: 47 | // * 删除掉指定的文件 ( 返回 false 即表示删除当前 target ) 48 | // * 返回新的文件内容 ( 返回 {content: '...'} ) 49 | // * 返回新的文件路径 ( 返回 {filePath: '...'},如此例所示 ) 50 | filter(target) { 51 | // template/example/config 目录下的文件需要到最外层去 52 | // 所以此处需要修改它的路径 53 | let dir = path.dirname(target.toPath) 54 | if (path.basename(dir) === 'config') { 55 | return { 56 | filePath: path.resolve(dir, '..', '..', target.name) 57 | } 58 | } 59 | return true 60 | }, 61 | overwrite: false // 如果有重名文件,是否覆盖它;不覆盖会新建一个 .backup 目录来存放原文件 62 | }, 63 | // ======================= 模板一 配置结束 ======================= 64 | 65 | 66 | /** 67 | * ================= 模板二 : 文件模板 ================= 68 | * 69 | * 当在项目的 widget 目录下新建 tsx 文件时,会自动使用 template/widget.tsx.dtpl 来生成文件内容 70 | */ 71 | { 72 | name: 'template/widget.tsx.dtpl', 73 | matches: '*-example/widget/**/*.tsx' 74 | }, 75 | 76 | // ======================= 模板二 配置结束 ======================= 77 | 78 | 79 | 80 | /** 81 | * ================= 模板三 : 创建关联文件,并生成其内容 ================= 82 | */ 83 | { 84 | name: 'template/page.tsx.dtpl', // 模板文件的路径 85 | matches: '*-example/page/**/*.tsx', // 匹配 example/page 下的所有后缀为 tsx 的文件 86 | /** 87 | * 创建和此 page 相关联的 样式文件 88 | * 89 | * 在同目录下的 style 文件夹下创建一个同名的 css 文件,并且在当前文件中插入 require('style/[name].css') 的引用 90 | */ 91 | related(data) { 92 | let styleFile = `./style/${data.fileName}.css` 93 | return { 94 | relativePath: styleFile, // 指定要新建文件的路径 95 | reference: `require('${styleFile}')`, // 要给 tsx 文件插入的引用 96 | // 自动插入引用在合适的地方,此配置只适用于 js/ts 文件对样式文件的引用 97 | // 如果是其它引用形式,可以通过指定 begin 和 end 坐标来插入到合适的地方 98 | smartInsertStyle: true 99 | } 100 | } 101 | }, 102 | 103 | // 给样式文件指定模板,这样当它被创建时,会使用此模板 104 | { 105 | name: 'template/page.css.dtpl', 106 | matches: '*-example/page/**/*.css' 107 | } 108 | // ======================= 模板三 配置结束 ======================= 109 | ], 110 | 111 | /** 112 | * 生成自定义的数据,在渲染模板时会使用,模板总共会从三处获取数据 113 | * 114 | * 1. 系统提供的文件本身的 basicData ,参考: https://github.com/qiu8310/dot-template#environment 115 | * 2. 用户配置的只有指定的模板才能用的 localData,可以在 templates 中的对象中配置 116 | * 3. 用户配置的所有模板都可以用的 globalData,如下所示 117 | * 118 | * 119 | * 注意,在创建三种不同的文件时,数据结构会有细微不一样 120 | * 121 | * - 文件夹复制 122 | * 123 | * 模板文件夹内的文件都没有 localData,但它可以通过 ref 获取到文件夹模板的 data 数据, 124 | * 而文件夹模板是可以包含 localData 的 125 | * 126 | * - 创建文本文件 127 | * 128 | * 文本文件默认的 data 会和 globalData 的数据 merge 129 | * 130 | * 131 | * - 创建关联文件 132 | * 133 | * 源文件和关联文件可能都会有它自己的模板,有它自己的 localData, 134 | * 所以它们的 data 会和各自的 localData 合并,有一点不一样的是, 135 | * 关联文件可以通过 ref 来引用源文件的所有 data 数据 136 | */ 137 | globalData: { 138 | projectName: '<%=ref.dirName%>' 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/fixtures/related/dtpl.js: -------------------------------------------------------------------------------- 1 | module.exports = function (source) { 2 | let EOL = '\n' 3 | let newline = 'newline' + EOL 4 | 5 | return { 6 | templates: [ 7 | { 8 | name: 'no-inject', 9 | matches: (minimatch) => { 10 | return source.filePath.indexOf('no-inject') >= 0 11 | }, 12 | related: () => { 13 | return [ 14 | { 15 | relativePath: './relative/no-inject' 16 | }, 17 | { 18 | relativePath: 'absolute/other' 19 | } 20 | ] 21 | } 22 | }, 23 | { 24 | name: 'inject', 25 | matches: 'template' 26 | }, 27 | { 28 | name: 'inject', 29 | matches: 'inject-start', 30 | related: () => { 31 | return [{ 32 | relativePath: './a', 33 | reference: newline 34 | }] 35 | } 36 | }, 37 | { 38 | name: 'inject', 39 | matches: 'inject-row', 40 | related: () => { 41 | return [{ 42 | relativePath: './a', 43 | reference: newline, 44 | begin: {row: 1, col: 0} 45 | }] 46 | } 47 | }, 48 | { 49 | name: 'inject', 50 | matches: 'inject-overflow-row', 51 | related: () => { 52 | return [{ 53 | relativePath: './a', 54 | reference: newline, 55 | begin: {row: 100, col: 0} 56 | }] 57 | } 58 | }, 59 | { 60 | name: 'inject', 61 | matches: 'inject-col', 62 | related: () => { 63 | return [{ 64 | relativePath: './a', 65 | reference: newline, 66 | begin: {row: 1, col: 4} 67 | }] 68 | } 69 | }, 70 | { 71 | name: 'inject', 72 | matches: 'inject-overflow-col', 73 | related: () => { 74 | return [{ 75 | relativePath: './a', 76 | reference: newline, 77 | begin: {row: 1, col: 400} 78 | }] 79 | } 80 | }, 81 | 82 | { 83 | name: 'inject', 84 | matches: 'inject-range-in-line', 85 | related: () => { 86 | return { 87 | relativePath: './a', 88 | reference: newline, 89 | begin: {row: 0, col: 0}, 90 | end: {row: 0, col: 2} 91 | } 92 | } 93 | }, 94 | 95 | { 96 | name: 'inject', 97 | matches: 'inject-range-in-line-overflow', 98 | related: () => { 99 | return { 100 | relativePath: './a', 101 | reference: newline, 102 | begin: {row: 0, col: 1}, 103 | end: {row: 0, col: 100} 104 | } 105 | } 106 | }, 107 | 108 | { 109 | name: 'inject', 110 | matches: 'inject-range-in-two-line', 111 | related: () => { 112 | return { 113 | relativePath: './a', 114 | reference: newline, 115 | begin: {row: 0, col: 0}, 116 | end: {row: 1, col: 2} 117 | } 118 | } 119 | }, 120 | 121 | { 122 | name: 'inject', 123 | matches: 'inject-range-in-two-line-overflow', 124 | related: () => { 125 | return { 126 | relativePath: './a', 127 | reference: newline, 128 | begin: {row: 0, col: 1}, 129 | end: {row: 4, col: 2} 130 | } 131 | } 132 | }, 133 | { 134 | name: 'inject', 135 | matches: 'inject-template', 136 | related: () => { 137 | return { 138 | relativePath: './template', 139 | reference: newline 140 | } 141 | } 142 | }, 143 | 144 | { 145 | name: 'replace', 146 | matches: 'replace*', 147 | related: () => { 148 | return [] 149 | } 150 | }, 151 | 152 | { 153 | name: 'style', 154 | matches: 'style1*', 155 | related(data) { 156 | return { 157 | relativePath: `./${data.fileName}.css`, 158 | reference: '---', 159 | smartInsertStyle: true 160 | } 161 | } 162 | }, 163 | { 164 | name: 'style', 165 | matches: 'style2*', 166 | related(data) { 167 | return { 168 | relativePath: `./${data.MODULE_NAME}.css`, 169 | reference: '---', 170 | smartInsertStyle: true 171 | } 172 | } 173 | }, 174 | { 175 | name: 'style', 176 | matches: 'style3*', 177 | related(data) { 178 | return { 179 | relativePath: `./style3.css`, 180 | reference: '---', 181 | smartInsertStyle: true 182 | } 183 | } 184 | } 185 | ] 186 | } 187 | 188 | } 189 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/commands/CreateDirectoriesCommand.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | 4 | import {Command, ICommandInitOptions} from './Command' 5 | import {Application} from '../Application' 6 | import {Template} from '../file/' 7 | import {series} from '../common' 8 | 9 | export class CreateDirectoriesCommand extends Command { 10 | private folders: string[] 11 | private exists: boolean[] = [] 12 | 13 | constructor(folders: string[], noInitError: boolean, app: Application, options: ICommandInitOptions) { 14 | super('CreateDirectoriesCommand', app, options) 15 | folders = this.filter(folders, true) 16 | 17 | // let templates: Template[] = [] 18 | // .forEach(f => { 19 | // let template = this.app.createSource(f).match(true) 20 | // if (template) templates.push(template) 21 | // }) 22 | 23 | this.folders = folders 24 | if (!folders.length) { 25 | this.invalid = true 26 | if (!noInitError) this.app.error('无任何可创建的有效文件夹:文件路径需要在项目内,并且文件夹需要不存在,或文件夹内无任何文件') 27 | } 28 | } 29 | 30 | async execute(): Promise { 31 | let {app} = this 32 | let {render, editor} = app 33 | await series(this.folders, async (toDir) => { 34 | let exists = fs.existsSync(toDir) 35 | if (!exists) fs.mkdirpSync(toDir) 36 | 37 | let tpl = this.app.createSource(toDir).match(true) as Template 38 | if (!tpl) { 39 | this.debug('目录 %f 没有匹配的模板', toDir) 40 | return 41 | } 42 | 43 | let fromDir = tpl.filePath 44 | 45 | let copiedFiles: string[] = [] 46 | 47 | this.debug('开始复制目录 %f => %f', fromDir, toDir) 48 | 49 | this.exists.push(exists) 50 | 51 | await walk(fromDir, async (rawName: string, fromPath: string, stats: fs.Stats) => { 52 | 53 | let relativePath = path.relative(fromDir, fromPath) 54 | 55 | let sourceData = tpl.data 56 | relativePath = render.removeFileEngineExtension(render.renderDtplContent(relativePath, sourceData)) 57 | 58 | let toPath = path.join(toDir, relativePath) 59 | let name = path.basename(toPath) 60 | 61 | let rawContent = '' 62 | let content = '' 63 | 64 | rawContent = editor.getFileContent(fromPath) 65 | let renderData = app.createSource(toPath).basicData 66 | renderData.ref = sourceData 67 | content = render.renderContent(rawContent, renderData, render.judgeEngineByFileExtension(fromPath)) 68 | 69 | let filterResult = tpl.filter({fromDir, toDir, fromPath, toPath, rawContent, rawName, name, relativePath, stats, content}) 70 | 71 | if (filterResult === false || filterResult == null) return false 72 | if (typeof filterResult === 'object') { 73 | if (filterResult.content != null) content = filterResult.content 74 | if (filterResult.filePath) { 75 | toPath = path.resolve(editor.rootPath, filterResult.filePath) 76 | } else if (filterResult.name && name !== filterResult.name) { 77 | toPath = path.join(path.dirname(toPath), filterResult.name) 78 | } 79 | } 80 | 81 | if (fromPath === toPath || toPath.indexOf(fromDir) === 0) { 82 | this.app.error(this.app.format('新生成的文件 %f 不能在源文件夹内 %f,已忽略', toPath, fromDir)) 83 | return false 84 | } 85 | 86 | // 备份已经存在的文件 87 | if (!tpl.custom.overwrite && fs.existsSync(toPath)) { 88 | let backupDir = path.resolve(toPath, '..', '.backup') 89 | fs.mkdirpSync(backupDir) 90 | fs.renameSync(toPath, path.join(backupDir, path.basename(toPath))) 91 | } 92 | await this.createFileAsync(toPath, content) 93 | copiedFiles.push(toPath) 94 | this.debug('复制文件 %f => %f', fromPath, toPath) 95 | 96 | return true 97 | }) 98 | tpl.afterFilter(fromDir, toDir, copiedFiles) 99 | 100 | await this.inject(tpl) 101 | 102 | this.debug('目录复制完成') 103 | }) 104 | 105 | return true 106 | } 107 | 108 | async rollback(): Promise { 109 | await series(this.folders, async (toDir, index) => { 110 | this.debug('删除目录 %f 的文件', toDir) 111 | await this.remove(toDir, !this.exists[index]) 112 | }) 113 | return true 114 | } 115 | 116 | async remove(dir: string, removeCurrent: boolean = false) { 117 | let names = fs.readdirSync(dir) 118 | await series(names, async (name) => { 119 | let f = path.join(dir, name) 120 | let stats = fs.statSync(f) 121 | if (stats.isDirectory()) { 122 | await this.remove(f, true) 123 | } else { 124 | await this.unlinkFileAsync(f, this.app.editor.getFileContent(f)) 125 | } 126 | }) 127 | if (removeCurrent) fs.removeSync(dir) 128 | } 129 | } 130 | 131 | 132 | async function walk(dir: string, fn: (name: string, filePath: string, stats: fs.Stats) => Promise) { 133 | let names = fs.readdirSync(dir) 134 | let filePath: string 135 | let stats: fs.Stats 136 | 137 | for (let name of names) { 138 | filePath = path.join(dir, name) 139 | stats = fs.statSync(filePath) 140 | 141 | if (stats.isFile()) { 142 | await fn(name, filePath, stats) 143 | } else if (stats.isDirectory()) { 144 | await walk(filePath, fn) 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/inc/Project.ts: -------------------------------------------------------------------------------- 1 | import {fixturesPath, path, fs, TestEditor, Application, assert} from './helper' 2 | 3 | export type EventType = 'createdFile' | 'deletedFile' | 'updatedFile' 4 | export interface ListenedItem { 5 | type: EventType, args: any[] 6 | } 7 | export class Project { 8 | rootPath: string 9 | editor: TestEditor 10 | app: Application 11 | 12 | listens: ListenedItem[] = [] 13 | constructor(name: string, dtplFolderName?: string, output: boolean = false) { 14 | this.rootPath = path.resolve(fixturesPath, '..', 'project-' + name) 15 | fs.ensureDirSync(this.rootPath) 16 | fs.emptyDirSync(this.rootPath) 17 | 18 | this.editor = new TestEditor(this.rootPath, output) 19 | 20 | if (dtplFolderName) { 21 | let target = path.join(this.rootPath, dtplFolderName) 22 | fs.ensureDirSync(target) 23 | fs.copySync(path.join(fixturesPath, dtplFolderName), target) 24 | this.editor.configuration.dtplFolderName = dtplFolderName 25 | } 26 | 27 | this.app = new Application(this.editor) 28 | 29 | let listener = (type: EventType) => { 30 | return (...args: any[]) => this.listens.push({type, args}) 31 | } 32 | 33 | this.app.onCreatedFile(listener('createdFile')) 34 | this.app.onDeletedFile(listener('deletedFile')) 35 | this.app.onUpdatedFile(listener('updatedFile')) 36 | } 37 | 38 | // 事件是异步触发的 39 | async matchListens(length: number, matches: Array<{type: EventType, args?: any[], fn?: ((item: ListenedItem) => boolean)}> = []) { 40 | return new Promise((resolve) => { 41 | setTimeout(() => { 42 | assert.equal(this.listens.length, length) 43 | for (let i = 0; i < matches.length; i++) { 44 | let lis = this.listens[i] 45 | let mat = matches[i] 46 | assert.equal(lis.type, mat.type, `listen event type not match`) 47 | if (mat.args) { 48 | assert(mat.args.length <= lis.args.length, 'expect too many listen args') 49 | for (let j = 0; j < mat.args.length; j++) { 50 | if (typeof mat.args[j] === 'string') { 51 | let arg = lis.args[j] 52 | arg = typeof arg === 'string' && arg.indexOf(this.rootPath) === 0 ? arg.substr(this.rootPath.length + 1) : arg 53 | assert.equal(mat.args[j], arg.trim(), `expect arg item not match`) 54 | } 55 | } 56 | } 57 | if (mat.fn) { 58 | assert(mat.fn(lis), 'listen callback return not true') 59 | } 60 | } 61 | resolve() 62 | }, 100) 63 | }) 64 | } 65 | 66 | fullPath(file: string) { 67 | return path.resolve(this.rootPath, file) 68 | } 69 | 70 | writeFile(file: string, content: string = '') { 71 | let fullPath = this.fullPath(file) 72 | fs.ensureDirSync(path.dirname(fullPath)) 73 | fs.writeFileSync(fullPath, content) 74 | } 75 | 76 | deleteFile(file: string) { 77 | let fullPath = this.fullPath(file) 78 | fs.statSync(fullPath).isFile() 79 | ? fs.unlinkSync(fullPath) 80 | : fs.removeSync(fullPath) 81 | } 82 | 83 | fileShouldExists(file: string) { 84 | assert(fs.existsSync(this.fullPath(file)), `file ${file} not exists`) 85 | } 86 | fileShouldNotExists(file: string) { 87 | assert(!fs.existsSync(this.fullPath(file)), `file ${file} exists`) 88 | } 89 | 90 | fileShouldBeTextFile(file: string) { 91 | this.fileShouldExists(file) 92 | assert(fs.statSync(this.fullPath(file)).isFile(), `file ${file} not a text file`) 93 | } 94 | 95 | getFileContent(file: string) { 96 | this.fileShouldBeTextFile(file) 97 | return fs.readFileSync(this.fullPath(file)).toString() 98 | } 99 | 100 | fileShouldOpened(file: string) { 101 | assert(this.editor.isOpened(this.fullPath(file)), `file ${file} should opened`) 102 | } 103 | fileShouldNotOpened(file: string) { 104 | assert(!this.editor.isOpened(this.fullPath(file)), `file ${file} should not opened`) 105 | } 106 | 107 | fileShouldMatch(file: string, match: RegExp | string, exact?: boolean) { 108 | let content = this.getFileContent(file) 109 | if (!exact) content = content.trim() 110 | if (typeof match === 'string') { 111 | assert.equal(content, match, `file ${file} content ${JSON.stringify(content)} not match ${JSON.stringify(match)}`) 112 | } else { 113 | assert(match.test(content), `file ${file} content ${JSON.stringify(content)} not match ${match}`) 114 | } 115 | } 116 | 117 | async createTemplateFilesAsync(files: string | string[], result: boolean, open: boolean = false) { 118 | files = Array.isArray(files) ? files : [files] 119 | files = files.map(f => f ? this.fullPath(f) : f) // 需要测试创建空文件的情况 120 | let res = await this.app.createTemplateFiles(files, open) 121 | assert.equal(res, result, 'createTemplateFiles result not match') 122 | } 123 | 124 | async createRelatedFilesAsync(file: string, result: boolean) { 125 | let res = await this.app.createRelatedFiles(this.fullPath(file)) 126 | assert.equal(res, result, 'createRelatedFiles result not match') 127 | } 128 | 129 | async createDirectoriesAsync(files: string | string[], result: boolean) { 130 | files = Array.isArray(files) ? files : [files] 131 | let res = await this.app.createDirectories(files.map(f => this.fullPath(f))) 132 | assert.equal(res, result, 'createRelatedFiles result not match') 133 | } 134 | 135 | async unredoAsync(result: boolean) { 136 | let res = await this.app.undoOrRedo() 137 | assert.equal(res, result, 'undo redo result not match') 138 | } 139 | 140 | destroy() { 141 | fs.removeSync(this.rootPath) 142 | } 143 | } 144 | 145 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dot-template 2 | 3 | [![Travis Build Status][travis-image]][travis-url] 4 | [![Appveyor Build Status][appveyor-image]][appveyor-url] 5 | 6 | **新建文件或文件夹时,根据预定义的配置和模板,自动向新文件里插入模板内容,或在新文件夹里生成新文件** 7 | 8 | 9 | ## 主要功能 10 | 11 | * 自动写入项目模板中的文件 12 | * 自动注入文件模板到新建的文件中 13 | * 生成某一文件的关联文件 14 | * 注入内容到指定的文件中(**新功能**) 15 | 16 | 17 | ## 如何使用 18 | 19 | 1. vscode 插件中查找并安装 `dot-template-vscode` 20 | 2. 重启 vscode 21 | 3. 在当前打开的项目中的根目录上创建一个 `.dtpl` 的文件夹(然后跟随 vscode 弹出的提示做即可) 22 | 23 | [更多详情请参考此](./ARTICLE_ABOUT_IT.md) 24 | 25 | 26 | 31 | 32 | 33 | 34 | ## 项目配置 35 | 36 | 37 | * `dot-template-vscode.debug`: 设置是否输出调试信息在项目根目录中的 dtpl.debug.log 文件中 38 | * `dot-template-vscode.noExampleWhenCreateDtplFolder`: 新建 .dtpl 文件夹时不要创建演示用的模板 39 | * `dot-template-vscode.watchFilesGlobPattern`: 指定要监听的文件,使用了 minimatch 匹配,并开启了 dot=true,其它选项默认 40 | 默认值: `"**/*"` 41 | * `dot-template-vscode.commandInvalidTimeout`: 设置命令的有效时间,过期后就无法撤销或重新执行,单位毫秒 42 | 默认值: `60000` 43 | * `dot-template-vscode.dtplFolderName`: 文件夹的名称,用于存放模板文件及相关配置文件 44 | 默认值: `".dtpl"` 45 | * `dot-template-vscode.dtplExtension`: 指定 dtpl 模板文件的后缀名 46 | 默认值: `".dtpl"` 47 | * `dot-template-vscode.ejsExtension`: 指定 ejs 模板文件的后缀名 48 | 默认值: `".ejs"` 49 | * `dot-template-vscode.njkExtension`: 指定 nunjucks 模板文件的后缀名 50 | 默认值: `".njk"` 51 | * `dot-template-vscode.minimatchOptions`: minimatch 的选项,用于匹配模板名称, 参考:https://github.com/isaacs/minimatch#options 52 | 默认值: `{"matchBase":true,"nocomment":true,"dot":true}` 53 | 54 | 55 | 56 | 57 | ## 支持命令 58 | 59 | 60 | * `createTemplateFiles`: DTPL: Create template files 61 | - win 快捷键: `ctrl+k ctrl+p` 62 | - mac 快捷键: `cmd+k cmd+p` 63 | 64 | 创建模板文件 65 | 1. 如果当前编辑器没有打开的文件,则会弹出输入框,可以输入你要创建的文件; 66 | 2. 如果当前打开的文件没内容,则会去寻找合适的模板来渲染; 67 | 3. 如果当前打开的文件有内容,则会去寻找合适的关联文件来创建 68 | 69 | * `createRelatedFiles`: DTPL: Create related files 70 | - win 快捷键: `ctrl+k ctrl+s` 71 | - mac 快捷键: `cmd+k cmd+s` 72 | 73 | 创建当前编辑器打开的文件的关联文件,如果当前编辑器没打开任何文件,则会报错 74 | 75 | * `undoOrRedo`: DTPL: Undo or Redo last action 76 | - win 快捷键: `ctrl+k ctrl+u` 77 | - mac 快捷键: `cmd+k cmd+u` 78 | 79 | 撤销或重做上次命令所做的所有修改,并且一分钟内才有效,超不一分钟无法撤销或重做(主要为了避免误操作) 80 | 81 | 82 | 83 | 84 | 85 | ## 渲染模板时的基本的环境变量 IData 86 | 87 | 88 | **Variable** | **Type** | **Nullable** | **Description** 89 | -----------------------|---------------------------|-----------------|-------------------------------------------------------------- 90 | `rootPath` | `string` | | 项目根目录的绝对路径 91 | `npmPath` | `string` | | 项目下的 node_modules 目录的绝对路径 92 | `date` | `string` | | 当前日期,格式:yyyy-mm-dd 93 | `time` | `string` | | 当前时间,格式: hh-mm 94 | `datetime` | `string` | | 当前日期和时间,格式:yyyy-mm-dd hh-mm 95 | `user` | `string` | | 当前用户,通过读取环境变量中的 USER 字段而获取到的 96 | `pkg` | `{[key: string]: any}` | | 当前项目的 package.json 所对应的 JSON 对象 97 | `filePath` | `string` | | 当前文件的绝对路径 98 | `relativeFilePath` | `string` | | 当前文件相对于根目录的路径 99 | `fileName` | `string` | | 当前文件的名称,不带路径和后缀 100 | `fileExt` | `string` | | 当前文件的后缀名 101 | `dirPath` | `string` | | 当前文件所在的目录的绝对路径 102 | `dirName` | `string` | | 当前文件所在的目录的名称 103 | `rawModuleName` | `string` | | fileName 的别名,即当前文件的名称(不含后缀) 104 | `moduleName` | `string` | | 驼峰形式的 fileName 105 | `ModuleName` | `string` | | 单词首字母都大写的形式的 fileName 106 | `MODULE_NAME` | `string` | | 所有字母都大写,中间以下划线连接的 fileName 107 | `module_name` | `string` | | 所有字母都小写,中间以下划线连接的 fileName 108 | `ref` | `IData` | Yes |创建 related 文件时,原文件的 IData 对象;或者创建文件夹模板内的文件时,文件夹的 IData 对象 109 | 110 | 111 | 除了上面的基本环境变量外,用户也可以在配置文件中定义 `globalData` 这个针对所有模板的变量,或者在单独的模板定义 `localData` 这个只针对本模板的局部变量 112 | 113 | 118 | 119 | 120 | [travis-image]: https://travis-ci.org/qiu8310/dot-template.svg?branch=master 121 | [travis-url]: https://travis-ci.org/qiu8310/dot-template 122 | [appveyor-image]: https://ci.appveyor.com/api/projects/status/0uegrq3stbmhvcwj?svg=true 123 | [appveyor-url]: https://ci.appveyor.com/project/qiu8310/dot-template 124 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/commands/Command.ts: -------------------------------------------------------------------------------- 1 | import {Application} from '../Application' 2 | import {Template} from '../file/Template' 3 | import * as fs from 'fs-extra' 4 | import * as path from 'path' 5 | import {unique, series} from '../common' 6 | import * as inject from 'mora-scripts/libs/fs/inject' 7 | 8 | export interface ICommandInitOptions { 9 | /** 10 | * 指定过期时间,即如过超过这个时间就无法 execute 或 rollback 11 | */ 12 | timeout?: number 13 | } 14 | 15 | /** 16 | * 命令组件有效期过了,无法再执行或回滚 17 | */ 18 | export class CommandTimeoutError extends Error { 19 | constructor(command: Command, expiredSeconds: number) { 20 | super(`命令已经过期 ${expiredSeconds}s 了,无法再 ${command.status === CommandStatus.INIT ? '执行' : '回滚'}`) 21 | } 22 | } 23 | 24 | export enum CommandStatus { 25 | /** 已经完成初始化 */ 26 | INIT, 27 | /** 已经执行过了,没有 rollback, rollback 即回到了 INIT */ 28 | EXECUTED 29 | } 30 | 31 | export abstract class Command { 32 | /** 33 | * 当前命令的状态,是刚初始化还是已经执行过了 34 | */ 35 | public status: CommandStatus 36 | 37 | /** 38 | * 标识当前命令是否有效 39 | * 40 | * 在命令初始化时,可能会有些非法参数,这时可以将 Command 设置成 invalid, 41 | * 这样就不会被记录到 Commander 中 42 | * 43 | * @type {boolean} 44 | * @memberof Command 45 | */ 46 | public invalid: boolean = false 47 | /** 48 | * 上次运行时间 49 | */ 50 | private lastRunTimestamp?: number 51 | 52 | constructor (public name: string, protected app: Application, private options: ICommandInitOptions) { 53 | this.status = CommandStatus.INIT 54 | } 55 | 56 | /** 57 | * 输出 debug 信息 58 | */ 59 | protected debug(message: string, ...files: string[]) { 60 | this.app.debug(`<${this.name}> ${message}`, ...files) 61 | } 62 | 63 | /** 64 | * 对文本文件或文件夹过滤,需要文件为空 65 | * 66 | * 文本文件为空: 文件不存在,或者文件内容为空 67 | * 文件夹为空: 文件不存在,或者文件夹内无其它文件 68 | * 69 | * @param filePaths 相对于根目录的文件路径 70 | * @param isDirectory 是要过滤文本文件还是目录 71 | */ 72 | protected filter(filePaths: string[], isDirectory: boolean) { 73 | const {rootPath, editor} = this.app 74 | 75 | let isFileEmpty = (file: string) => !fs.existsSync(file) || fs.statSync(file).isFile() && editor.getFileContent(file).trim() === '' 76 | let isDirEmpty = (file: string) => !fs.existsSync(file) || fs.statSync(file).isDirectory() && fs.readdirSync(file).length === 0 77 | 78 | filePaths = filePaths 79 | .filter(f => !!f) 80 | .map(f => path.resolve(rootPath, f)) 81 | .filter(f => { 82 | // 文件必须要在项目文件夹内,并且需要为空 83 | return f.indexOf(rootPath) === 0 && (isDirectory ? isDirEmpty(f) : isFileEmpty(f)) 84 | }) 85 | 86 | return unique(filePaths) 87 | } 88 | 89 | protected async inject(tpl: Template) { 90 | await series(tpl.getInjects(), async ({data, file, append, eol, tags = 'loose' as 'loose'}) => { 91 | this.app.debug('inject %f', file) 92 | if (fs.existsSync(file)) { 93 | let c = inject(file, data, {tags, append, eol, returnContent: true}) as string 94 | await this.app.editor.setFileContentAsync(file, c) 95 | } 96 | }) 97 | } 98 | 99 | protected async createFileAsync(file: string, content: string) { 100 | fs.ensureDirSync(path.dirname(file)) 101 | fs.writeFileSync(file, content) 102 | this.app.emitCreatedFile(file, content) 103 | } 104 | protected async setFileContentAsync(file: string, newContent: string, oldContent: string): Promise { 105 | if (newContent === oldContent) return true 106 | let {app} = this 107 | if (await app.editor.setFileContentAsync(file, newContent) === false) { 108 | app.error(app.format('设置文件 %f 内容失败', file)) 109 | return false 110 | } else { 111 | app.emitUpdatedFile(file, newContent, oldContent) 112 | return true 113 | } 114 | } 115 | protected async unlinkFileAsync(file: string, fileContent: string) { 116 | try { 117 | await this.app.editor.closeFileAsync(file) 118 | } catch (e) {} 119 | fs.unlinkSync(file) 120 | this.app.emitDeletedFile(file, fileContent) 121 | } 122 | 123 | /** 124 | * 命令是否可运行 125 | * 126 | * @readonly 127 | * @type {boolean} 128 | */ 129 | // get runnable(): boolean { 130 | // let {options: {timeout}, ?} = this 131 | // let now = Date.now() 132 | // return !(timeout && timeout > 0 && lastRunTimestamp && now - lastRunTimestamp > timeout) 133 | // } 134 | 135 | /** 136 | * 运行命令 137 | * @param {boolean} forward true 表示 execute, false 表示 rollback 138 | */ 139 | async run(forward: boolean): Promise { 140 | let {options: {timeout}, lastRunTimestamp} = this 141 | let now = Date.now() 142 | if (timeout && timeout > 0 && lastRunTimestamp && now - lastRunTimestamp > timeout) { 143 | throw new CommandTimeoutError(this, Math.max(Math.round((now - lastRunTimestamp - timeout) / 100), 1)) 144 | } 145 | 146 | let result: boolean 147 | if (forward) { 148 | result = await this.execute() 149 | if (result) this.status = CommandStatus.EXECUTED 150 | } else { 151 | result = await this.rollback() 152 | if (result) this.status = CommandStatus.INIT 153 | } 154 | 155 | if (result === true) this.lastRunTimestamp = now 156 | return result 157 | } 158 | 159 | /** 160 | * 161 | * 执行当前命令 162 | * 163 | * @abstract 164 | * @returns {Promise} 命令是否执行成功 165 | * 166 | * - true:表示执行正常,会将命令打入历史记录 167 | * - false: 表示执行不正常,不会将命令打入历史记录 168 | * 169 | * @throws 抛出异常的话,命令也不会打入历史记录 170 | * 171 | * @memberof Command 172 | */ 173 | protected abstract async execute(): Promise 174 | 175 | /** 176 | * 177 | * 回滚当前命令 178 | * 179 | * @abstract 180 | * @returns {Promise} 181 | * 182 | * - true:表示执行正常,会将命令打入历史记录 183 | * - false: 表示执行不正常,不会将命令打入历史记录 184 | * 185 | * @throws 抛出异常的话,命令也不会打入历史记录 186 | * 187 | * @memberof Command 188 | */ 189 | protected abstract async rollback(): Promise 190 | } 191 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/test_createDirectories.ts: -------------------------------------------------------------------------------- 1 | import {Project, File, assert} from './inc/' 2 | import * as fs from 'fs-extra' 3 | 4 | let pro: Project 5 | async function createDir(name: string, create: boolean, result: boolean): Promise { 6 | let f = new File(name, pro) 7 | if (create) f.dir() 8 | await f.createDirectories(result) 9 | return f 10 | } 11 | 12 | describe('createDirectories init', () => { 13 | beforeEach(() => pro = new Project('createDirectories', 'js')) 14 | afterEach(() => pro.destroy()) 15 | it('should return false when filePath is not directory', async () => { 16 | let f = new File('xx', pro) 17 | f.write('') 18 | await f.createDirectories(false) 19 | }) 20 | it('should return false when directory has files in it', async () => { 21 | let f = new File('xx/a', pro) 22 | f.write('') 23 | await createDir('xx', false, false) 24 | }) 25 | it('should return false when directory is not in project', async () => { 26 | await createDir('../xx', false, false) 27 | }) 28 | 29 | it('should return true if directory not exists and not template', async () => { 30 | await createDir('xx', false, true) 31 | }) 32 | it('should return true if directory is empty and not template', async () => { 33 | await createDir('xx', true, true) 34 | }) 35 | 36 | it('should return true if directory not exists and has template', async () => { 37 | await createDir('dir', false, true) 38 | }) 39 | it('should return true if directory is empty and has template', async () => { 40 | await createDir('dir', true, true) 41 | }) 42 | }) 43 | 44 | describe('createDirectories execute and events', () => { 45 | beforeEach(() => pro = new Project('createDirectories', 'js')) 46 | afterEach(() => pro.destroy()) 47 | it('should create dir and files', async () => { 48 | await createDir('dir', false, true) 49 | let f1 = new File('dir/text.txt', pro) 50 | let f2 = new File('dir/DIR.js', pro) 51 | let f3 = new File('dir/foo/readme.md', pro) 52 | 53 | f1.shouldMatch('${MODULE_NAME}') 54 | f2.shouldMatch('${fileName}') 55 | f3.shouldMatch('README ref:DIR') 56 | await pro.matchListens(3, [{type: 'createdFile'}, {type: 'createdFile'}, {type: 'createdFile'}]) 57 | }) 58 | 59 | it('should create dir and files', async () => { 60 | await createDir('dir-foo', true, true) 61 | new File('dir-foo/text.txt', pro).shouldMatch('${MODULE_NAME}') 62 | new File('dir-foo/DIR_FOO.js', pro).shouldMatch('${fileName}') 63 | new File('dir-foo/foo/readme.md', pro).shouldMatch('README ref:DIR_FOO') 64 | 65 | await pro.matchListens(3, [{type: 'createdFile'}, {type: 'createdFile'}, {type: 'createdFile'}]) 66 | }) 67 | 68 | it('filter-text', async () => { 69 | let d = await createDir('filter-text', false, true) 70 | let f1 = new File('filter-text/text.txt', pro) 71 | f1.shouldMatch('${MODULE_NAME}') 72 | await pro.matchListens(1, [{type: 'createdFile'}]) 73 | await pro.unredoAsync(true) 74 | await pro.matchListens(2, [{type: 'createdFile'}, {type: 'deletedFile'}]) 75 | f1.shouldNotExists() 76 | d.shouldNotExists() 77 | }) 78 | 79 | it('filter-content', async () => { 80 | let d = await createDir('filter-content', true, true) 81 | let f1 = new File('filter-content/text.txt', pro) 82 | f1.shouldMatch('hack') 83 | await pro.matchListens(1, [{type: 'createdFile'}]) 84 | await pro.unredoAsync(true) 85 | await pro.matchListens(2, [{type: 'createdFile'}, {type: 'deletedFile'}]) 86 | f1.shouldNotExists() 87 | d.shouldExists() 88 | }) 89 | 90 | it('filter-name', async () => { 91 | await createDir('filter-name', true, true) 92 | let f1 = new File('filter-name/hack', pro) 93 | f1.shouldMatch('${MODULE_NAME}') 94 | }) 95 | 96 | it('filter-content-name', async () => { 97 | await createDir('filter-content-name', true, true) 98 | let f1 = new File('filter-content-name/hack', pro) 99 | f1.shouldMatch('hack') 100 | }) 101 | 102 | it('filter-path', async () => { 103 | await createDir('filter-path', true, true) 104 | let f1 = new File('hack', pro) 105 | f1.shouldMatch('${MODULE_NAME}') 106 | }) 107 | 108 | it('filter-path-invalid', async () => { 109 | await createDir('filter-path-invalid', true, true) 110 | assert.equal(pro.editor.test_errors.length, 1) 111 | }) 112 | }) 113 | 114 | describe('createDirectories rollback and events', () => { 115 | beforeEach(() => pro = new Project('createDirectories', 'js')) 116 | afterEach(() => pro.destroy()) 117 | it('should revert1', async () => { 118 | let d = await createDir('dir', false, true) 119 | let f1 = new File('dir/text.txt', pro) 120 | let f2 = new File('dir/DIR.js', pro) 121 | let f3 = new File('dir/foo/readme.md', pro) 122 | 123 | await pro.unredoAsync(true) 124 | 125 | f1.shouldNotExists() 126 | f2.shouldNotExists() 127 | f3.shouldNotExists() 128 | await pro.matchListens(6) 129 | d.shouldNotExists() 130 | }) 131 | 132 | it('should revert2', async () => { 133 | let d = await createDir('dir', true, true) 134 | let f1 = new File('dir/text.txt', pro) 135 | let f2 = new File('dir/DIR.js', pro) 136 | let f3 = new File('dir/foo/readme.md', pro) 137 | 138 | await pro.unredoAsync(true) 139 | 140 | f1.shouldNotExists() 141 | f2.shouldNotExists() 142 | f3.shouldNotExists() 143 | await pro.matchListens(6) 144 | d.shouldExists() 145 | }) 146 | }) 147 | 148 | 149 | describe('create .dtpl folder', () => { 150 | beforeEach(() => pro = new Project('createDirectories', 'js')) 151 | afterEach(() => pro.destroy()) 152 | 153 | it('should create system folder', async () => { 154 | pro.editor.configuration.dtplFolderName = '.dtpl' 155 | let d = await createDir('.dtpl', true, true) 156 | assert(fs.readdirSync(d.path).length > 0) 157 | }) 158 | 159 | it('should create system specified folder', async () => { 160 | pro.editor.configuration.dtplFolderName = 'xx' 161 | let d = await createDir('xx', true, true) 162 | assert(fs.readdirSync(d.path).length > 0) 163 | }) 164 | }) 165 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/Application.ts: -------------------------------------------------------------------------------- 1 | import * as Events from 'mora-scripts/libs/lang/Events' 2 | import * as fs from 'fs-extra' 3 | import * as path from 'path' 4 | import * as minimatch from 'minimatch' 5 | import {Editor} from './Editor' 6 | import {Commander} from './Commander' 7 | import {Render} from './Render' 8 | import {Source} from './file/' 9 | import {getIgnore} from './common' 10 | 11 | export class Application { 12 | private event: Events 13 | private cmder: Commander 14 | 15 | public render: Render 16 | public rootPath: string 17 | public dotTemplateRootPath: string = path.resolve(__dirname, '..', '..') 18 | 19 | constructor(public editor: Editor) { 20 | this.event = new Events() 21 | this.render = new Render(this) 22 | this.cmder = new Commander(this, 1) 23 | this.editor.app = this 24 | this.rootPath = editor.rootPath 25 | 26 | // 监听新建文件或者文件夹的操作 27 | let sid: NodeJS.Timer 28 | let newFiles: string[] = [] 29 | let run = async () => { 30 | let files: string[] = [] 31 | let folders: string[] = [] 32 | 33 | newFiles.forEach((f, i) => { 34 | // 文件必须要存在,要不然不知道是文件还是目录 35 | if (fs.existsSync(f)) { 36 | let stats = fs.statSync(f) 37 | if (stats.isFile()) { 38 | files.push(f) 39 | } else if (stats.isDirectory()) { 40 | folders.push(f) 41 | } 42 | } 43 | }) 44 | newFiles.length = 0 45 | 46 | // 有可能是重命名文件夹,所以不用打开文件 47 | files.length && await this.createTemplateFiles(files, false, true) 48 | folders.length && await this.createDirectories(folders, true) 49 | } 50 | 51 | let isRunning = false 52 | let includeMatcher = new minimatch.Minimatch(this.editor.configuration.watchFilesGlobPattern, {dot: true}) 53 | 54 | this.event.on('newFile', (filePath: string) => { 55 | // 执行命令时会创建新文件,会被检测到,要忽略它 56 | if (isRunning && this.cmder.fileMaybeCreatedByCommand()) return 57 | let relativePath = path.relative(this.rootPath, filePath) 58 | if (!includeMatcher.match(relativePath) || getIgnore(this.rootPath).ignores(relativePath)) { 59 | return 60 | } 61 | 62 | if (sid) clearTimeout(sid) 63 | if (newFiles.indexOf(filePath) < 0) { 64 | newFiles.push(filePath) 65 | this.debug('监听到新建了文件 %f', filePath) 66 | } 67 | sid = setTimeout(async () => { 68 | isRunning = true 69 | try { 70 | await run() 71 | isRunning = false 72 | } catch (e) { 73 | isRunning = false 74 | this.error(e.message, e) 75 | } 76 | }, 300) // 再等待会儿,批量处理 77 | }) 78 | 79 | this.debug('Application rootPath ' + this.rootPath) 80 | this.debug('Application initialized') 81 | } 82 | 83 | createRelatedFiles = async (textFile: string) => await this.cmder.addCreateRelatedFilesCommand(textFile) 84 | createDirectories = async (folders: string[], noInitError: boolean = false) => { 85 | let result = await this.cmder.addCreateDirectoriesCommand(folders, noInitError) 86 | if (result) { 87 | let folder = folders.find(f => path.basename(f) === this.editor.configuration.dtplFolderName) 88 | if (folder) { 89 | let tip = this.editor.configuration.noExampleWhenCreateDtplFolder ? '' : ',快去该目录下查看 readme.md 文件吧' 90 | this.info('恭喜,成功创建 dot-template 配置文件夹 %f' + tip, folder) 91 | } 92 | } 93 | return result 94 | } 95 | createTemplateFiles = async (textFile: string[], open: boolean, noInitError: boolean = false) => await this.cmder.addCreateTemplateFilesCommand(textFile, open, noInitError) 96 | undoOrRedo = async () => this.cmder.hasPrev ? await this.cmder.prev() : await this.cmder.next() 97 | 98 | runUserFunction(name: string, fn: (...args: any[]) => T, args: any[] = [], context?: any): T | undefined { 99 | let result: T | undefined 100 | try { 101 | result = fn.apply(context, args) 102 | } catch(e) { 103 | this.error('运行自定义函数 ' + name + ' 出错:' + (e && e.message ? e.message : JSON.stringify(e)), e) 104 | } 105 | return result 106 | } 107 | 108 | createSource(filePath: string): Source { 109 | return new Source(this, filePath) 110 | } 111 | 112 | /** 113 | * 将 message 中的 %f 用 files 的相对路径来替换 114 | */ 115 | format(message: string, ...files: string[]): string { 116 | let {editor} = this 117 | if (files.length) { 118 | message = message.replace(/%f/g, _ => { 119 | let file = files.shift() 120 | if (!file) return _ 121 | if (file.indexOf(this.rootPath) >= 0) return editor.getRelativeFilePath(file) 122 | if (file.indexOf(this.dotTemplateRootPath) >= 0) return '' + file.substr(this.dotTemplateRootPath.length) 123 | return file 124 | }) 125 | } 126 | return message 127 | } 128 | 129 | debug(message: string, ...files: string[]) { 130 | this.editor.debug(this.format(message, ...files)) 131 | } 132 | info(message: string, ...files: string[]) { 133 | this.editor.info(this.format(message, ...files)) 134 | } 135 | warning(message: string, ...files: string[]) { 136 | this.editor.warning(this.format(message, ...files)) 137 | } 138 | error(message: string, e?: any) { 139 | this.editor.error(message, e) 140 | } 141 | 142 | /* istanbul ignore next */ 143 | dispose() { 144 | this.editor.dispose() 145 | this.editor.debug('Application destroyed') 146 | } 147 | 148 | emitNewFile = (filePath: string) => this.event.emit('newFile', filePath) 149 | emitCreatedFile = (filePath: string, content: string) => this.event.emit('createdFile', filePath, content) 150 | emitDeletedFile = (filePath: string, content: string) => this.event.emit('deletedFile', filePath, content) 151 | emitUpdatedFile = (filePath: string, newContent: string, oldContent: string) => this.event.emit('updatedFile', filePath, newContent, oldContent) 152 | onCreatedFile = (listener: (filePath: string, content: string) => void) => this.event.on('createdFile', listener) 153 | onDeletedFile = (listener: (filePath: string, content: string) => void) => this.event.on('deletedFile', listener) 154 | onUpdatedFile = (listener: (filePath: string, newContent: string, oldContent: string) => void) => this.event.on('updatedFile', listener) 155 | } 156 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dot-template-vscode", 3 | 4 | "data": { 5 | "rootPath": { 6 | "type": "string", 7 | "description": "项目根目录的绝对路径" 8 | }, 9 | "npmPath": { 10 | "type": "string", 11 | "description": "项目下的 node_modules 目录的绝对路径" 12 | }, 13 | "date": { 14 | "type": "string", 15 | "description": "当前日期,格式:yyyy-mm-dd" 16 | }, 17 | "time": { 18 | "type": "string", 19 | "description": "当前时间,格式: hh-mm" 20 | }, 21 | "datetime": { 22 | "type": "string", 23 | "description": "当前日期和时间,格式:yyyy-mm-dd hh-mm" 24 | }, 25 | "user": { 26 | "type": "string", 27 | "description": "当前用户,通过读取环境变量中的 USER 字段而获取到的" 28 | }, 29 | "pkg": { 30 | "type": "{[key: string]: any}", 31 | "description": "当前项目的 package.json 所对应的 JSON 对象" 32 | }, 33 | 34 | "filePath": { 35 | "type": "string", 36 | "description": "当前文件的绝对路径" 37 | }, 38 | "relativeFilePath": { 39 | "type": "string", 40 | "description": "当前文件相对于根目录的路径" 41 | }, 42 | "fileName": { 43 | "type": "string", 44 | "description": "当前文件的名称,不带路径和后缀" 45 | }, 46 | "fileExt": { 47 | "type": "string", 48 | "description": "当前文件的后缀名" 49 | }, 50 | "dirPath": { 51 | "type": "string", 52 | "description": "当前文件所在的目录的绝对路径" 53 | }, 54 | "dirName": { 55 | "type": "string", 56 | "description": "当前文件所在的目录的名称" 57 | }, 58 | "rawModuleName": { 59 | "type": "string", 60 | "description": "fileName 的别名,即当前文件的名称(不含后缀)" 61 | }, 62 | "moduleName": { 63 | "type": "string", 64 | "description": "驼峰形式的 fileName" 65 | }, 66 | "ModuleName": { 67 | "type": "string", 68 | "description": "单词首字母都大写的形式的 fileName" 69 | }, 70 | "MODULE_NAME": { 71 | "type": "string", 72 | "description": "所有字母都大写,中间以下划线连接的 fileName" 73 | }, 74 | "module_name": { 75 | "type": "string", 76 | "description": "所有字母都小写,中间以下划线连接的 fileName" 77 | }, 78 | "ref": { 79 | "optional": true, 80 | "type": "IData", 81 | "description": "创建 related 文件时,原文件的 IData 对象;或者创建文件夹模板内的文件时,文件夹的 IData 对象" 82 | } 83 | }, 84 | 85 | "commands": { 86 | "createTemplateFiles": { 87 | "title": "DTPL: Create template files", 88 | "key": "ctrl+k ctrl+p", 89 | "mac": "cmd+k cmd+p", 90 | "desc": "创建模板文件 \n1. 如果当前编辑器没有打开的文件,则会弹出输入框,可以输入你要创建的文件;\n2. 如果当前打开的文件没内容,则会去寻找合适的模板来渲染;\n3. 如果当前打开的文件有内容,则会去寻找合适的关联文件来创建" 91 | }, 92 | 93 | "createRelatedFiles": { 94 | "title": "DTPL: Create related files", 95 | "key": "ctrl+k ctrl+s", 96 | "mac": "cmd+k cmd+s", 97 | "desc": "创建当前编辑器打开的文件的关联文件,如果当前编辑器没打开任何文件,则会报错" 98 | }, 99 | 100 | "undoOrRedo": { 101 | "title":"DTPL: Undo or Redo last action", 102 | "key": "ctrl+k ctrl+u", 103 | "mac": "cmd+k cmd+u", 104 | "desc": "撤销或重做上次命令所做的所有修改,并且一分钟内才有效,超不一分钟无法撤销或重做(主要为了避免误操作)" 105 | } 106 | }, 107 | 108 | "options": { 109 | "debug": { 110 | "type": "boolean", 111 | "default": false, 112 | "description": "设置是否输出调试信息在项目根目录中的 dtpl.debug.log 文件中" 113 | }, 114 | "noExampleWhenCreateDtplFolder": { 115 | "type": "boolean", 116 | "default": false, 117 | "description": "新建 .dtpl 文件夹时不要创建演示用的模板" 118 | }, 119 | "watchFilesGlobPattern": { 120 | "type": "string", 121 | "default": "**/*", 122 | "description": "指定要监听的文件,使用了 minimatch 匹配,并开启了 dot=true,其它选项默认" 123 | }, 124 | "commandInvalidTimeout": { 125 | "type": "number", 126 | "default": 60000, 127 | "description": "设置命令的有效时间,过期后就无法撤销或重新执行,单位毫秒" 128 | }, 129 | "dtplFolderName": { 130 | "type": "string", 131 | "default": ".dtpl", 132 | "description": "文件夹的名称,用于存放模板文件及相关配置文件" 133 | }, 134 | "dtplExtension": { 135 | "type": "string", 136 | "default": ".dtpl", 137 | "description": "指定 dtpl 模板文件的后缀名" 138 | }, 139 | "ejsExtension": { 140 | "type": "string", 141 | "default": ".ejs", 142 | "description": "指定 ejs 模板文件的后缀名" 143 | }, 144 | "njkExtension": { 145 | "type": "string", 146 | "default": ".njk", 147 | "description": "指定 nunjucks 模板文件的后缀名" 148 | }, 149 | 150 | "minimatchOptions": { 151 | "type": "object", 152 | "default": { 153 | "matchBase": true, 154 | "nocomment": true, 155 | "dot": true 156 | }, 157 | "description": "minimatch 的选项,用于匹配模板名称, 参考:https://github.com/isaacs/minimatch#options", 158 | "properties": { 159 | "nobrace": { 160 | "type": "boolean", 161 | "description": "Do not expand {a,b} and {1..3} brace sets" 162 | }, 163 | "noglobstar": { 164 | "type": "boolean", 165 | "description": "Disable ** matching against multiple folder names" 166 | }, 167 | "dot": { 168 | "type": "boolean", 169 | "description": "Allow patterns to match filenames starting with a period, even if the pattern does not explicitly have a period in that spot" 170 | }, 171 | "noext": { 172 | "type": "boolean", 173 | "description": "Disable 'extglob style patterns like +(a|b)" 174 | }, 175 | "nocase": { 176 | "type": "boolean", 177 | "description": "Perform a case-insensitive match" 178 | }, 179 | "matchBase": { 180 | "type": "boolean", 181 | "description": "If set, then patterns without slashes will be matched against the basename of the path if it contains slashes" 182 | }, 183 | "nocomment": { 184 | "type": "boolean", 185 | "description": "Suppress the behavior of treating # at the start of a pattern as a comment" 186 | }, 187 | "nonegate": { 188 | "type": "boolean", 189 | "description": "Suppress the behavior of treating a leading ! character as negation" 190 | }, 191 | "flipNegate": { 192 | "type": "boolean", 193 | "description": "Returns from negate expressions the same as if they were not negated. (Ie, true on a hit, false on a miss.)" 194 | } 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/commands/CreateRelatedFilesCommand.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'fs-extra' 2 | import * as path from 'path' 3 | 4 | import {Command, ICommandInitOptions} from './Command' 5 | import {Application} from '../Application' 6 | import {IRelated, series, unique} from '../common' 7 | import {Source} from '../file/' 8 | 9 | interface IExtendedRelated extends IRelated { 10 | filePath: string 11 | } 12 | 13 | export interface IPoint { 14 | row: number 15 | col: number 16 | } 17 | 18 | export class CreateRelatedFilesCommand extends Command { 19 | private relatedSources: IExtendedRelated[] 20 | private infos: Array<{content: string, injected: boolean}> = [] 21 | /** 22 | * 初始文件的 Source 23 | */ 24 | // @ts-ignore 25 | private source: Source 26 | 27 | /** 28 | * 创建关联文件 29 | * 30 | * relatedFiles 所关联的文件都是不存在的 31 | */ 32 | constructor(textFile: string, app: Application, options: ICommandInitOptions) { 33 | super('CreateRelatedFilesCommand', app, options) 34 | let rs: IExtendedRelated[] = [] 35 | let {rootPath} = app 36 | if (fs.existsSync(textFile) && fs.statSync(textFile).isFile()) { 37 | let source = this.app.createSource(textFile) 38 | this.source = source 39 | 40 | let tpl = source.match(false) 41 | rs = (tpl ? tpl.getRelatedSources() : []) 42 | .map(r => { 43 | let p = r.relativePath 44 | let filePath = path.resolve(p[0] === '.' ? path.dirname(source.filePath) : rootPath, p) 45 | return {...r, filePath} 46 | }) 47 | rs = rs.filter(r => r.filePath && r.filePath.indexOf(rootPath) === 0) // 确保关联的文件不存在并且要在项目中 48 | } 49 | 50 | rs = unique(rs, 'filePath') 51 | if (!rs.length) { 52 | this.invalid = true 53 | this.app.error(this.app.format('没有需要创建的 %f 的关联文件', textFile)) 54 | } 55 | let notExistsRs = rs.filter(r => !fs.existsSync(r.filePath)) 56 | this.relatedSources = notExistsRs 57 | if (!notExistsRs.length) { 58 | rs.forEach(r => { 59 | if (fs.existsSync(r.filePath)) { 60 | this.app.editor.openFileAsync(r.filePath) 61 | } 62 | }) 63 | } 64 | } 65 | 66 | private replace(content: string, replacer: string, begin: IPoint, end: IPoint | null): string { 67 | // 将换行符放在最后当作一个字符 68 | let lines = content.split(/\r?\n/).map((line, i, all) => i === all.length - 1 ? line : line + this.app.editor.EOL) 69 | 70 | if (begin.row >= lines.length) { 71 | return content + replacer 72 | } 73 | 74 | return lines.reduce((res: string[], line, i) => { 75 | if (i === begin.row) { 76 | let prefix = line.substr(0, begin.col) + replacer 77 | if (!end) { 78 | res.push(prefix + line.substr(begin.col)) 79 | } else if (i === end.row) { 80 | res.push(prefix + line.substr(end.col)) 81 | } else { 82 | res.push(prefix) 83 | } 84 | } else if (i > begin.row && end && i <= end.row) { 85 | if (i === end.row) { 86 | res.push(line.substr(end.col)) 87 | } 88 | } else { 89 | res.push(line) 90 | } 91 | return res 92 | }, []).join('') 93 | } 94 | 95 | async execute(): Promise { 96 | const {app} = this 97 | let {editor, render} = app 98 | let sourceFilePath = this.source.filePath 99 | let result = await series(this.relatedSources, async (r) => { 100 | this.debug('开始创建文件 %f', r.filePath) 101 | 102 | await this.createFileAsync(r.filePath, '') 103 | let tpl = app.createSource(r.filePath).match(false) 104 | if (tpl) { 105 | let data = tpl.data 106 | data.ref = this.source.basicData 107 | await this.setFileContentAsync(r.filePath, render.renderFile(tpl.filePath, data), '') 108 | } 109 | 110 | // 注入引用 111 | let {reference, begin, end, smartInsertStyle} = r 112 | let injected = !!reference 113 | let content = '' 114 | if (reference) { 115 | content = editor.getFileContent(sourceFilePath) 116 | if (begin == null && end == null && smartInsertStyle && editor.isJsFileOrTsFile(sourceFilePath)) { 117 | // 计算出 begin 坐标 118 | let rtn = calculateStartInjectPoint(content, reference) 119 | begin = rtn.begin 120 | end = rtn.end 121 | if (!end) reference = editor.EOL + reference + editor.EOL 122 | } 123 | 124 | let s = {row: 0, col: 0, ...(begin || {})} 125 | let e = end ? {...s, ...end} : null 126 | await this.setFileContentAsync(sourceFilePath, this.replace(content, reference, s, e), content) 127 | } 128 | 129 | await editor.openFileAsync(r.filePath) 130 | this.infos.push({injected, content}) 131 | return true 132 | }) 133 | 134 | return result.every(r => r) 135 | } 136 | 137 | async rollback(): Promise { 138 | let {editor} = this.app 139 | let sourceFilePath = this.source.filePath 140 | 141 | let result = await series(this.relatedSources, async (r, i) => { 142 | if (fs.existsSync(r.filePath)) { 143 | this.debug('开始撤销文件 %f' + r.filePath) 144 | await this.unlinkFileAsync(r.filePath, editor.getFileContent(r.filePath)) 145 | this.debug('文件 %f 撤销成功', r.filePath) 146 | } 147 | 148 | // 撤消注入的引用 149 | if (fs.existsSync(sourceFilePath)) { 150 | let info = this.infos[i] 151 | if (info.injected && info.content) { 152 | await this.setFileContentAsync(sourceFilePath, info.content, editor.getFileContent(sourceFilePath)) 153 | } 154 | } 155 | 156 | return true 157 | }) 158 | 159 | return result.every(r => r) 160 | } 161 | } 162 | 163 | export function calculateStartInjectPoint(content: string, reference: string): {begin: IPoint, end?: IPoint} { 164 | let startLine = 0 165 | // 去掉文件开头的注释行 166 | if (/^(\s*\/\*[\s\S]*?\*\/)/.test(content)) { 167 | startLine = RegExp.$1.split(/\r?\n/).length 168 | } 169 | let lines = content.split(/\r?\n/) 170 | 171 | let begin: IPoint | undefined 172 | let end: IPoint | undefined 173 | 174 | const requireRegExp = /^\s*(\/\/)?\s*(import|(var|let|const)\s+\w+\s+=\s+require)\b/ 175 | 176 | let lastImportLineNumber = startLine 177 | for (let i = startLine; i < lines.length; i++) { 178 | let text = lines[i] 179 | if (text.indexOf(reference) >= 0) { 180 | begin = {row: i, col: 0} 181 | end = {row: i, col: text.length} 182 | } 183 | if (requireRegExp.test(text)) lastImportLineNumber = i 184 | } 185 | 186 | if (begin && end) { 187 | return {begin, end} 188 | } else { 189 | return {begin: {row: lastImportLineNumber + 1, col: 0}} 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/test/core/test_createRelated.ts: -------------------------------------------------------------------------------- 1 | 2 | import {Project, File} from './inc/' 3 | 4 | let pro: Project 5 | async function createFile(name: string, content: string, result: boolean): Promise { 6 | let f = new File(name, pro) 7 | f.write(content) 8 | await f.related(result) 9 | return f 10 | } 11 | 12 | describe('createRelated init', () => { 13 | beforeEach(() => pro = new Project('createRelated', 'related', false)) 14 | afterEach(() => pro.destroy()) 15 | 16 | it('should return false when file is not exists or file is not text file', async () => { 17 | let f = new File('xxx', pro) 18 | await f.related(false) 19 | 20 | f.dir() 21 | await f.related(false) 22 | }) 23 | 24 | it('should return false when no related source', async () => { 25 | await createFile('xxx', '', false) 26 | }) 27 | 28 | it('should return false when has related source but file not exists', async () => { 29 | let f = new File('no-inject', pro) 30 | await f.related(false) 31 | }) 32 | 33 | it('should return true when file exists and has related source', async () => { 34 | await createFile('no-inject', 'any', true) 35 | }) 36 | }) 37 | 38 | describe('createRelated execute & event', () => { 39 | beforeEach(() => pro = new Project('createRelated', 'related')) 40 | afterEach(() => pro.destroy()) 41 | 42 | it('related files should be created and rendered', async () => { 43 | await createFile('a/no-inject', '', true) 44 | let f1 = new File('a/relative/no-inject', pro) 45 | let f2 = new File('absolute/other', pro) 46 | f1.shouldExists() 47 | f2.shouldExists() 48 | 49 | f1.shouldMatch('no-inject content') 50 | f2.shouldMatch('') 51 | 52 | await pro.matchListens(3, [{type: 'createdFile'}, {type: 'updatedFile'}, {type: 'createdFile'}]) 53 | }) 54 | 55 | it('inject-start', async () => { 56 | let s = await createFile('a/inject-start', 'line1\nline2', true) 57 | new File('a/a', pro).shouldExists() 58 | s.shouldMatch('newline\nline1\nline2', true) 59 | }) 60 | 61 | it('inject-row', async () => { 62 | let s = await createFile('inject-row', 'line1\nline2', true) 63 | new File('a', pro).shouldExists() 64 | s.shouldMatch('line1\nnewline\nline2', true) 65 | }) 66 | 67 | it('inject-overflow-row', async () => { 68 | let s = await createFile('inject-overflow-row', 'line1\nline2\n', true) 69 | new File('a', pro).shouldExists() 70 | s.shouldMatch('line1\nline2\nnewline\n', true) 71 | }) 72 | 73 | it('inject-col', async () => { 74 | let s = await createFile('inject-col', 'line1\nline2', true) 75 | new File('a', pro).shouldExists() 76 | s.shouldMatch('line1\nlinenewline\n2') 77 | }) 78 | 79 | it('inject-overflow-col', async () => { 80 | let s = await createFile('inject-overflow-col', 'line1\nline2', true) 81 | new File('a', pro).shouldExists() 82 | s.shouldMatch('line1\nline2newline\n', true) 83 | }) 84 | 85 | it('inject-range-in-line', async () => { 86 | let s = await createFile('inject-range-in-line', 'line1\nline2', true) 87 | new File('a', pro).shouldExists() 88 | s.shouldMatch('newline\nne1\nline2', true) 89 | }) 90 | 91 | 92 | it('inject-range-in-line-overflow', async () => { 93 | let s = await createFile('inject-range-in-line-overflow', 'line1\nline2', true) 94 | new File('a', pro).shouldExists() 95 | s.shouldMatch('lnewline\nline2', true) 96 | }) 97 | 98 | it('inject-range-in-two-line', async () => { 99 | let s = await createFile('inject-range-in-two-line', 'line1\nline2', true) 100 | new File('a', pro).shouldExists() 101 | s.shouldMatch('newline\nne2', true) 102 | }) 103 | 104 | it('inject-range-in-two-line-overflow', async () => { 105 | let s = await createFile('inject-range-in-two-line-overflow', 'line1\nline2\n', true) 106 | new File('a', pro).shouldExists() 107 | s.shouldMatch('lnewline\n', true) 108 | }) 109 | 110 | it('inject source and template related', async () => { 111 | let s = await createFile('inject-template', 'line1\nline2', true) 112 | let f = new File('template', pro) 113 | f.shouldExists() 114 | f.shouldMatch('template') 115 | s.shouldMatch('newline\nline1\nline2', true) 116 | }) 117 | 118 | it('inject-style1', async () => { 119 | let s = await createFile('style1.js', '/**\n * foo\n * bar\n */\n', true) 120 | let f = new File('style1.css', pro) 121 | f.shouldExists() 122 | s.shouldMatch('/**\n * foo\n * bar\n */\n\n---\n', true) 123 | }) 124 | 125 | it('inject-style2', async () => { 126 | let s = await createFile('style2.js', '\nconst a = require("...")\nconst b = require("xxx")\n\nmodule.exports = () => {}', true) 127 | 128 | let f = new File('STYLE_2.css', pro) 129 | f.shouldExists() 130 | s.shouldMatch('\nconst a = require("...")\nconst b = require("xxx")\n\n---\n\nmodule.exports = () => {}', true) 131 | }) 132 | 133 | it('inject-style3', async () => { 134 | let s = await createFile('style3.js', '\n\/\/ ---\n', true) 135 | 136 | let f = new File('style3.css', pro) 137 | f.shouldExists() 138 | s.shouldMatch('\n---\n', true) 139 | }) 140 | 141 | }) 142 | 143 | describe('createRelated rollback', () => { 144 | beforeEach(() => pro = new Project('createRelated', 'related')) 145 | afterEach(() => pro.destroy()) 146 | 147 | it('related files should be revoke', async () => { 148 | await createFile('a/no-inject', '', true) 149 | let f1 = new File('a/relative/no-inject', pro) 150 | let f2 = new File('absolute/other', pro) 151 | f1.shouldExists() 152 | f2.shouldExists() 153 | 154 | await pro.unredoAsync(true) 155 | 156 | await pro.matchListens(5, [{type: 'createdFile'}, {type: 'updatedFile'}, {type: 'createdFile'}, {type: 'deletedFile'}, {type: 'deletedFile'}]) 157 | }) 158 | 159 | it('related files should not be revoke if it was deleted', async () => { 160 | await createFile('a/no-inject', '', true) 161 | let f1 = new File('a/relative/no-inject', pro) 162 | let f2 = new File('absolute/other', pro) 163 | f1.shouldExists() 164 | f2.shouldExists() 165 | 166 | f1.delete() 167 | 168 | await pro.unredoAsync(true) 169 | 170 | await pro.matchListens(4, [{type: 'createdFile'}, {type: 'updatedFile'}, {type: 'createdFile'}, {type: 'deletedFile'}]) 171 | }) 172 | 173 | it('revoke inject-start', async () => { 174 | let s = await createFile('inject-start', 'line1\nline2', true) 175 | new File('a', pro).shouldExists() 176 | s.shouldMatch('newline\nline1\nline2', true) 177 | 178 | await pro.unredoAsync(true) 179 | s.shouldMatch('line1\nline2', true) 180 | }) 181 | 182 | it('will not revoke inject-start, if file is deleted', async () => { 183 | let s = await createFile('inject-start', 'line1\nline2', true) 184 | new File('a', pro).shouldExists() 185 | s.shouldMatch('newline\nline1\nline2', true) 186 | 187 | s.delete() 188 | 189 | await pro.unredoAsync(true) 190 | s.shouldNotExists() 191 | }) 192 | }) 193 | 194 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/src/adapter/VscodeEditor.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode' 2 | import * as path from 'path' 3 | import * as os from 'os' 4 | import * as fs from 'fs-extra' 5 | 6 | import {Editor, IConfiguration} from 'dot-template-core' 7 | 8 | const jsLanguageIds = ['typescriptreact', 'javascriptreact', 'typescript', 'javascript'] 9 | const ID = 'dot-template-vscode' 10 | 11 | export class VscodeEditor extends Editor { 12 | // @ts-ignore 13 | public configuration: IConfiguration 14 | 15 | // private fileSystemWatcher: vscode.FileSystemWatcher // 和 App 中重复了 16 | private configListener: vscode.Disposable 17 | 18 | constructor() { 19 | // 获取一个包含 .dtpl 的文件夹,把它当作 rootPath 20 | let c = vscode.workspace.getConfiguration(ID) 21 | let dtplFolderName = c.get('dtplFolderName', '.dtpl') 22 | let folders = (vscode.workspace.workspaceFolders || []).map(f => f.uri.fsPath) 23 | let rootPath = folders 24 | ? (folders.find(dir => fs.existsSync(path.join(dir, dtplFolderName))) || folders[0]) 25 | : process.cwd() 26 | super(rootPath) 27 | this.setConfiguration(c) 28 | this.configListener = vscode.workspace.onDidChangeConfiguration(() => this.setConfiguration()) 29 | // this.fileSystemWatcher = vscode.workspace.createFileSystemWatcher('**/*', false, true, true) 30 | // this.fileSystemWatcher.onDidCreate(uri => this.app.emitNewFile(uri.fsPath)) 31 | } 32 | 33 | private setConfiguration(c?: vscode.WorkspaceConfiguration) { 34 | c = c || vscode.workspace.getConfiguration(ID) 35 | let eol = vscode.workspace.getConfiguration('files', null as any).get('eol', os.EOL) 36 | if (!['\r', '\n', '\r\n'].includes(eol)) eol = os.EOL 37 | 38 | this.EOL = eol 39 | this.configuration = { 40 | debug: c.get('debug', false), 41 | noExampleWhenCreateDtplFolder: c.get('noExampleWhenCreateDtplFolder', false), 42 | watchFilesGlobPattern: c.get('watchFilesGlobPattern', '**/*'), 43 | commandInvalidTimeout: c.get('commandInvalidTimeout', 60000), 44 | dtplFolderName: c.get('dtplFolderName', '.dtpl'), 45 | minimatchOptions: c.get('minimatchOptions') || {matchBase: true, nocomment: true, dot: true}, 46 | templateExtensions: { 47 | ejs: c.get('ejsExtension', '.ejs'), 48 | dtpl: c.get('dtplExtension', '.dtpl'), 49 | njk: c.get('njkExtension', '.njk') 50 | } 51 | } 52 | 53 | this.debug('配置信息更新 ' + JSON.stringify(this.configuration)) 54 | } 55 | 56 | dispose() { 57 | // this.fileSystemWatcher.dispose() 58 | this.configListener.dispose() 59 | } 60 | 61 | async confirm(message: string): Promise { 62 | let picks = ['确认', '取消'] 63 | let chose = await vscode.window.showQuickPick(picks, {placeHolder: message}) 64 | return chose === picks[0] 65 | } 66 | 67 | getRelativeFilePath(file: string) { 68 | return path.relative(this.rootPath, file) 69 | } 70 | 71 | /** 72 | * 文件是否是 js 文件 73 | * 74 | * 如果在 vscode 中可以通过判断 languageId 来准确得到 75 | * 76 | * @param {string} file 77 | */ 78 | isJsFileOrTsFile(file: string): boolean { 79 | let editor = getFileEditor(file) 80 | if (editor) return jsLanguageIds.includes(editor.document.languageId) 81 | return /\.[jt]sx?$/i.test(file) 82 | } 83 | 84 | /** 85 | * 打开文件 86 | */ 87 | async openFileAsync(file: string): Promise { 88 | let editor = getFileEditor(file) 89 | if (!editor) { 90 | editor = await vscode.window.showTextDocument(await vscode.workspace.openTextDocument(file)) 91 | } else if (editor !== vscode.window.activeTextEditor) { 92 | editor = await vscode.window.showTextDocument(editor.document) 93 | } 94 | 95 | // vscode 有两种打开,一种是临时打开,在打开下一个临时文件时会覆盖上一个 96 | // 此时当然不是临时打开,需要一直打开着 97 | await vscode.commands.executeCommand(`workbench.action.keepEditor`) 98 | return this.isOpened(file) 99 | } 100 | 101 | /** 102 | * 关闭文件 103 | */ 104 | async closeFileAsync(file: string): Promise { 105 | let editor = getFileEditor(file) 106 | if (!editor) return true 107 | if (editor.document.isDirty && false === (await editor.document.save())) { 108 | return false 109 | } 110 | 111 | await vscode.commands.executeCommand(`workbench.action.closeActiveEditor`) 112 | return !this.isOpened(file) 113 | } 114 | 115 | /** 116 | * 设置文件内容 117 | * 118 | * @param {string} file 119 | * @param {string} content 120 | */ 121 | async setFileContentAsync(file: string, content: string): Promise { 122 | let editor = getFileEditor(file) 123 | if (editor) { 124 | return await setEditorContentAsync(editor, content) 125 | } else { 126 | fs.writeFileSync(file, content) 127 | return true 128 | } 129 | } 130 | 131 | /** 132 | * 同步获取文件的内容 133 | * 134 | * @param {string} file 135 | * @returns 136 | * @memberof Editor 137 | */ 138 | getFileContent(file: string) { 139 | let editor = getFileEditor(file) 140 | if (editor) { 141 | return editor.document.getText() 142 | } 143 | return fs.readFileSync(file).toString() 144 | } 145 | 146 | /** 147 | * 判断文件是否打开了 148 | */ 149 | isOpened(file: string): boolean { 150 | return vscode.window.visibleTextEditors.some(editor => editor.document.fileName === file) 151 | } 152 | 153 | debug(message: string) { 154 | if (this.configuration.debug) { 155 | console.log('[dtpl] ' + message) 156 | let debugFile = path.join(this.rootPath, 'dtpl.debug.log') 157 | fs.ensureFileSync(debugFile) 158 | fs.writeFile(debugFile, message + this.EOL, {flag: 'a'}) 159 | } 160 | } 161 | warning(message: string) { 162 | vscode.window.showWarningMessage(`[dtpl] ${message}`) 163 | } 164 | info(message: string) { 165 | vscode.window.showInformationMessage(`[dtpl] ${message}`) 166 | } 167 | error(message: string, e?: Error | any) { 168 | let hasError = !!e 169 | message = message.split(/\r?\n/)[0] // 取第一行 170 | vscode.window.showErrorMessage(`[dtpl] ${message} ${hasError ? '(详情请查看根目录下 dtpl.error.log 文件)' : ''}`) 171 | if (hasError) { 172 | console.error(e) 173 | let errorFile = path.join(this.rootPath, 'dtpl.error.log') 174 | fs.ensureFileSync(errorFile) 175 | fs.writeFile(errorFile, e.stack || JSON.stringify(e)) 176 | } 177 | } 178 | } 179 | 180 | 181 | function getFileEditor(file: string) { 182 | return vscode.window.visibleTextEditors.find(editor => editor.document.fileName === file) 183 | } 184 | 185 | async function setEditorContentAsync(editor: vscode.TextEditor, content: string): Promise { 186 | // insertSnippet 时会替换 $xxx 为空(如果没有匹配到 vscode 的变量的化) 187 | // await editor.insertSnippet(new vscode.SnippetString(content), posOrRange) 188 | 189 | // 不要保存,保存了会触发 webpack 更新代码,这样不好 190 | // await editor.document.save() 191 | return editor.edit(builder => { 192 | let start = new vscode.Position(0, 0) 193 | let lastLine = editor.document.lineAt(editor.document.lineCount - 1) 194 | 195 | builder.replace(new vscode.Range(start, lastLine.range.end), content) 196 | }) 197 | } 198 | -------------------------------------------------------------------------------- /src/bin.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import * as info from 'mora-scripts/libs/sys/info' 4 | import * as error from 'mora-scripts/libs/sys/error' 5 | import * as cli from 'mora-scripts/libs/tty/cli' 6 | import * as xlog from 'mora-scripts/libs/sys/xlog' 7 | import * as _inject from 'mora-scripts/libs/fs/inject' 8 | import * as findup from 'mora-scripts/libs/fs/findup' 9 | import * as fs from 'fs-extra' 10 | import * as path from 'path' 11 | import * as os from 'os' 12 | import * as assert from 'assert' 13 | 14 | let rootPath = path.dirname(findup.pkg(__dirname)) 15 | let DOT_TEMPLATE_CORE_ROOT = path.join(rootPath, 'packages', 'dot-template-core') 16 | let DOT_TEMPLATE_VSCODE_ROOT = path.join(rootPath, 'packages', 'dot-template-vscode') 17 | 18 | interface IConfig { 19 | name: string 20 | data: {[key: string]: {optional?: boolean, type: string, description: string}} 21 | commands: {[key: string]: {title: string, key?: string, mac?: string, desc: string}} 22 | options: {[key: string]: {default?: any, type: string, description: string}} 23 | } 24 | 25 | cli({ 26 | usage: 'dtpl [options] ' 27 | }) 28 | .commands({ 29 | inject: { 30 | desc: xlog.format('根据 %cdot-template-core/src/config/config.json%c 文件的配置,给项目其它地方注入合适的值', 'yellow', 'reset'), 31 | cmd: function() { 32 | 33 | let readmePath = path.join(rootPath, 'README.md') 34 | let articlePath = path.join(rootPath, 'ARTICLE_ABOUT_IT.md') 35 | let vscodePackagePath = path.resolve(DOT_TEMPLATE_VSCODE_ROOT, 'package.json') 36 | let vscodeEntryPath = path.resolve(DOT_TEMPLATE_VSCODE_ROOT, 'src', 'vscode.ts') 37 | 38 | let coreInterfacePath = path.join(DOT_TEMPLATE_CORE_ROOT, 'src', 'common', 'interface.ts') 39 | let coreDataPath = path.join(DOT_TEMPLATE_CORE_ROOT, 'src', 'common', 'data.ts') 40 | let config: IConfig = require(DOT_TEMPLATE_CORE_ROOT + '/src/config/config.json') 41 | injectReadme(config, readmePath, articlePath) 42 | injectInterfaceAndData(config, coreInterfacePath, coreDataPath) 43 | injectPackageAndVscodeEntry(config, vscodePackagePath, vscodeEntryPath) 44 | 45 | // 复制 markdown 文件到 dot-template-vscode 中 46 | fs.copyFileSync(path.join(rootPath, 'README.md'), path.join(DOT_TEMPLATE_VSCODE_ROOT, 'README.md')) 47 | fs.copyFileSync(path.join(rootPath, 'CHANGELOG.md'), path.join(DOT_TEMPLATE_VSCODE_ROOT, 'CHANGELOG.md')) 48 | fs.copyFileSync(path.join(rootPath, 'ARTICLE_ABOUT_IT.md'), path.join(DOT_TEMPLATE_VSCODE_ROOT, 'ARTICLE_ABOUT_IT.md')) 49 | } 50 | } 51 | }) 52 | .parse(function() { 53 | return this.help() 54 | }) 55 | 56 | function injectPackageAndVscodeEntry({name, options, commands: cs}: IConfig, packagePath: string, vscodeEntryPath: string) { 57 | // configuration commands keybindings 58 | 59 | let registerCommands: string[] = [] 60 | 61 | let configuration = { 62 | title: name, 63 | type: 'object', 64 | properties: Object.keys(options).reduce((prev: {[key: string]: any}, key) => { 65 | prev[name + '.' + key] = options[key] 66 | return prev 67 | }, {}) 68 | } 69 | 70 | let activationEvents: string[] = [] 71 | let keybindings: Array<{command: string, key?: string, mac?: string}> = [] 72 | let commands = Object.keys(cs).map(key => { 73 | let command = name + '.' + key 74 | let c = cs[key] 75 | if (c.key || c.mac) keybindings.push({command, key: c.key, mac: c.mac}) 76 | 77 | activationEvents.push(`onCommand:${command}`) 78 | registerCommands.push(`vscode.commands.registerCommand('${command}', app.${key})`) 79 | return {command, title: cs[key].title} 80 | }, {}) 81 | 82 | let oldPkg = require(packagePath) 83 | let newPkg = JSON.parse(JSON.stringify(oldPkg)) 84 | 85 | newPkg.activationEvents = (newPkg.activationEvents || []).filter((e: string) => !e.startsWith('onCommand:')) 86 | newPkg.activationEvents.push(...activationEvents) 87 | newPkg.contributes = newPkg.contributes || {} 88 | newPkg.contributes.configuration = configuration 89 | newPkg.contributes.keybindings = keybindings 90 | newPkg.contributes.commands = commands 91 | 92 | let relative = path.relative(rootPath, packagePath) 93 | 94 | try { 95 | assert.deepEqual(oldPkg, newPkg) 96 | info(`文件 ${relative} 没有变化`) 97 | } catch(e) { 98 | fs.writeFileSync(packagePath, JSON.stringify(newPkg, null, 2)) 99 | info(`注入文件 ${relative} 成功`) 100 | } 101 | 102 | inject(vscodeEntryPath, {commands: registerCommands.join(',\n')}) 103 | } 104 | 105 | function injectInterfaceAndData({data}: IConfig, interfacePath: string, dataPath: string) { 106 | let dataExplain: string[] = [] 107 | let basicData = Object.keys(data).reduce((lines: string[], key) => { 108 | let d = data[key] 109 | lines.push('/**') 110 | lines.push(' * ' + d.description) 111 | lines.push(' * @type {' + d.type + '}') 112 | lines.push(' */') 113 | lines.push(`${key}${d.optional ? '?' : ''}: ${d.type}`) 114 | dataExplain.push(`${key}: {desc: '${d.description}', type: '${d.type}'}`) 115 | return lines 116 | }, []).join(os.EOL) 117 | 118 | inject(interfacePath, {basicData}) 119 | inject(dataPath, {dataExplain: dataExplain.join(',' + os.EOL)}) 120 | } 121 | 122 | function injectReadme({options, data, commands: cs, name}: IConfig, readmePath: string, articlePath: string) { 123 | // 项目配置: 124 | let configure = Object.keys(options).map(key => { 125 | let defaultValue = !options[key].default ? '' : os.EOL + ` 默认值: ${code(options[key].default, true)}` 126 | return `* ${code(name + '.' + key)}: ${options[key].description}` + defaultValue 127 | }).join(os.EOL) 128 | 129 | // 支持命令 130 | let commands = Object.keys(cs).map(key => { 131 | let command = cs[key] 132 | let lines = [`* ${code(key)}: ${command.title}`] 133 | if (command.key) { 134 | if (!command.mac) { 135 | lines.push(`快捷键: ${code(command.key)}`) 136 | } else { 137 | lines.push( 138 | ` - win 快捷键: ${code(command.key)}`, 139 | ` - mac 快捷键: ${code(command.mac)}` 140 | ) 141 | } 142 | lines.push('\n ' + command.desc.replace(/\n/mg, '\n ') + '\n') 143 | } 144 | return lines.join(os.EOL) 145 | }).join(os.EOL) 146 | 147 | // 环境变量 148 | let head = [['**Variable**', '**Type**', '**Nullable**', '**Description**']] 149 | let environment = table(head.concat(Object.keys(data).map(key => { 150 | let d = data[key] 151 | return [code(key), code(d.type), d.optional ? 'Yes' : '', d.description] 152 | }))) 153 | 154 | inject(readmePath, {configure, commands, environment}) 155 | inject(articlePath, {configure, commands, environment}) 156 | } 157 | 158 | function inject(file: string, data: {[key: string]: any}) { 159 | let relative = path.relative(rootPath, file) 160 | let count = _inject(file, data) 161 | if (count === Object.keys(data).length) info(`注入文件 ${relative} 成功`) 162 | else error(`注入文件 ${relative} 失败`) 163 | } 164 | 165 | function code(content: string, stringify: boolean = false) { 166 | return '`' + (stringify ? JSON.stringify(content) : content) + '`' 167 | } 168 | 169 | function table(rows: Array) { 170 | let sampleRow = rows[0] 171 | let columnNum = sampleRow.length 172 | rows.splice(1, 0, sampleRow.map(r => '')) 173 | 174 | let rowNum = rows.length 175 | 176 | let columnMaxWidths = new Array(columnNum) 177 | for (let i = 0; i < columnNum; i++) { 178 | columnMaxWidths[i] = Math.max(...rows.map(r => r[i].length)) + 3 179 | } 180 | 181 | let lines = [] 182 | for (let i = 0; i < rowNum; i++) { 183 | let line = '' 184 | let padChar = i === 1 ? '-' : ' ' 185 | for (let j = 0; j < columnNum; j++) { 186 | line += xlog.align(rows[i][j], '2.' + columnMaxWidths[j], {rightPad: padChar, leftPad: padChar}) 187 | + (j === columnNum - 1 ? '' : '|') 188 | } 189 | lines.push(line) 190 | } 191 | return lines.join(os.EOL) 192 | } 193 | -------------------------------------------------------------------------------- /packages/dot-template-core/src/core/file/Source.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path' 2 | import * as minimatch from 'minimatch' 3 | import * as fs from 'fs-extra' 4 | import {Template} from './Template' 5 | import {Application} from '../Application' 6 | import {IBasicData, IData, IDtplConfig, IUserTemplate, data, requireFile} from '../common' 7 | 8 | export class Source { 9 | private _basicData?: IBasicData 10 | 11 | relativeFilePath: string 12 | exists: boolean 13 | // stats: fs.Stats | undefined 14 | isFile: boolean 15 | isDirectory: boolean 16 | fileContent: string 17 | systemConfigDir: string 18 | 19 | constructor(public app: Application, public filePath: string) { 20 | let stats: fs.Stats | undefined 21 | try { 22 | stats = fs.statSync(filePath) 23 | } catch (e) {} 24 | 25 | this.exists = !!stats 26 | this.isFile = stats ? stats.isFile() : false 27 | this.isDirectory = stats ? stats.isDirectory() : false 28 | this.fileContent = this.isFile ? this.app.editor.getFileContent(filePath) : '' 29 | this.relativeFilePath = path.relative(this.app.rootPath, this.filePath) 30 | 31 | this.systemConfigDir = path.join(app.dotTemplateRootPath, 'out', 'config') 32 | } 33 | 34 | get basicData(): IBasicData { 35 | if (!this._basicData) { 36 | this._basicData = data(this.app.rootPath, this.filePath) 37 | } 38 | return this._basicData as IBasicData 39 | } 40 | 41 | createTemplate(filePath: string, data: IData, userTemplate: IUserTemplate) { 42 | return new Template(this, filePath, data, userTemplate) 43 | } 44 | 45 | match(isTemplateDirectory: boolean): Template | undefined { 46 | let {dtplFolderName} = this.app.editor.configuration 47 | let dtplFolders = this.findAllDirectoriesCanExistsDtplFolder().map(f => path.join(f, dtplFolderName)) 48 | 49 | // 如果刚创建的文件夹正好就是 .dtpl 目录 50 | // 则不应该在它下面找配置信息,要忽略它 51 | if (isTemplateDirectory && path.basename(this.filePath) === dtplFolderName) { 52 | dtplFolders = dtplFolders.filter(f => f.indexOf(this.filePath) !== 0) 53 | } 54 | 55 | dtplFolders.push(this.systemConfigDir) // dtpl 一定会使用的 .dtpl 目录 56 | for (let dtplFolder of dtplFolders) { 57 | if (fs.existsSync(dtplFolder)) { 58 | let configFile = this.findConfigFileInDtplFolder(dtplFolder) 59 | let config: IDtplConfig | undefined 60 | 61 | if (configFile) { 62 | this.app.debug('找到配置文件 %f', configFile) 63 | config = this.loadDtplConfig(configFile) 64 | } 65 | 66 | if (config) { 67 | let userTemplate = this.findMatchedUserTemplate(dtplFolder, configFile as string, isTemplateDirectory, config) 68 | 69 | if (userTemplate) { 70 | const templatePath = path.resolve(dtplFolder, userTemplate.name) 71 | this.app.debug(`找到匹配的模板文件 %f`, templatePath) 72 | 73 | return this.createTemplate(templatePath, {...this.basicData, ...(config.globalData || {}), ...(userTemplate.localData || {})}, userTemplate) 74 | } else { 75 | this.app.debug('配置文件 %f 中没有匹配的模板', configFile as string) 76 | } 77 | } 78 | } 79 | } 80 | 81 | this.app.debug(`没有找到和文件 %f 匹配的模板文件`, this.filePath) 82 | return 83 | } 84 | 85 | /** 86 | * 根据用户的配置,查找一个匹配的并且存在的模板文件 87 | */ 88 | private findMatchedUserTemplate(dtplFolder: string, configFile: string, isTemplateDirectory: boolean, config: IDtplConfig): IUserTemplate | undefined { 89 | let defaultMinimatchOptions = this.app.editor.configuration.minimatchOptions 90 | 91 | return config.templates.find(t => { 92 | let matches = Array.isArray(t.matches) ? t.matches : [t.matches] 93 | let templatePath = path.resolve(dtplFolder, t.name) 94 | 95 | let found: boolean = false 96 | if (templatePath === this.filePath && templatePath.endsWith('.dtpl')) { // 当前编辑的文件就是它自己,匹配成功,主要为了触发 dtpl 文件的自动补全 97 | found = true 98 | } else { 99 | found = !!matches.find(m => { 100 | let result = false 101 | if (typeof m === 'string') { 102 | if (t.minimatch === false) { 103 | result = this.relativeFilePath === m 104 | } else { 105 | result = minimatch(this.relativeFilePath, m, typeof t.minimatch !== 'object' ? defaultMinimatchOptions : t.minimatch) 106 | } 107 | } else if (typeof m === 'function') { 108 | result = !!this.app.runUserFunction('template.match', m, [minimatch, this], t) 109 | } else { 110 | this.app.warning(`配置文件 %f 中的模板 ${t.name} 中的 matches 配置错误,只允许字符串或函数`, configFile) 111 | result = false 112 | } 113 | 114 | if (dtplFolder !== this.systemConfigDir) { 115 | this.app.debug(`TEMPLATE: ${t.name} MATCH: ${m} 匹配${result ? '' : '不'}成功`) 116 | } 117 | return result 118 | }) 119 | } 120 | 121 | if (found) { 122 | if (!fs.existsSync(templatePath)) { 123 | this.app.warning(`模板 ${t.name} 对应的文件 %f 不存在,已忽略`, templatePath) 124 | found = false 125 | } else { 126 | let stats = fs.statSync(templatePath) 127 | if (stats.isFile() && isTemplateDirectory || stats.isDirectory() && !isTemplateDirectory) { 128 | // 很正常,不要报错了 129 | // this.app.warning(`模板 ${t.name} 对应的文件 %f ${isTemplateDirectory ? '应该是目录' : '不应该是目录'}`, templatePath) 130 | found = false 131 | } 132 | } 133 | } 134 | 135 | return found 136 | }) 137 | } 138 | 139 | /** 140 | * 加载配置文件,每次都重新加载,确保无缓存 141 | */ 142 | private loadDtplConfig(configFile: string): IDtplConfig | undefined { 143 | if (configFile.endsWith('.ts')) { 144 | let tsnode = path.join(this.app.rootPath, 'node_modules', 'ts-node') 145 | let tsc = path.join(this.app.rootPath, 'node_modules', 'typescript') 146 | if (fs.existsSync(tsnode) && fs.existsSync(tsc)) { 147 | require(path.join(tsnode, 'register')) // 不需要用 requireFile 148 | } else { 149 | this.app.error(this.app.format(`配置文件 %f 使用了 ts 后缀,但本地没有安装 ts-node 和 typescript,无法编译`, configFile)) 150 | return 151 | } 152 | } 153 | let mod: any = requireFile(configFile) 154 | let config: IDtplConfig | undefined 155 | if (mod) { 156 | let fn = mod.default ? mod.default : mod 157 | if (typeof fn === 'function') config = this.app.runUserFunction('dtpl config', fn, [this]) 158 | else if (typeof fn === 'object') config = fn 159 | else { 160 | this.app.warning(`配置文件 %f 配置错误,没有导出函数或对象`) 161 | return 162 | } 163 | } 164 | 165 | if (config && (!config.templates || !Array.isArray(config.templates))) { 166 | this.app.warning(`配置文件 %f 没有返回 templates 数组`, configFile) 167 | return 168 | } 169 | 170 | return config 171 | } 172 | 173 | /** 174 | * 在 dtpl 目录内找到配置文件 175 | */ 176 | private findConfigFileInDtplFolder(dtplFolder: string): string | undefined { 177 | let result: string | undefined 178 | let names = ['dtpl.cjs', 'dtpl.js', 'dtpl.ts'] 179 | 180 | for (let n of names) { 181 | let f = path.join(dtplFolder, n) 182 | if (fs.existsSync(f) && fs.statSync(f).isFile()) { 183 | result = f 184 | break 185 | } 186 | } 187 | 188 | if (!result) { 189 | this.app.warning(`目录 %f 里没有配置文件`, dtplFolder) 190 | } 191 | 192 | return result 193 | } 194 | 195 | /** 196 | * 不在递归向上查找 .dtpl 文件夹了(因为如果两个编辑器打开的项目共用一个 .dtpl 文件夹时,会出现问题) 197 | */ 198 | private findAllDirectoriesCanExistsDtplFolder(): string[] { 199 | let result = [this.app.rootPath] 200 | 201 | // let dir = this.filePath // 不用管它是文件还是文件夹 202 | // result.push(dir) 203 | 204 | // while (dir !== path.resolve(dir, '..')) { 205 | // dir = path.resolve(dir, '..') 206 | // result.push(dir) 207 | // } 208 | 209 | if (process.env.HOME && result.indexOf(process.env.HOME) < 0) result.push(process.env.HOME) 210 | return result 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /packages/dot-template-vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dot-template-vscode", 3 | "displayName": "Dot Template", 4 | "description": "create new files according to pre-defined templates", 5 | "version": "0.4.0", 6 | "license": "MIT", 7 | "publisher": "qiu8310", 8 | "scripts": { 9 | "clear": "remove-all out", 10 | "build": "npm run clear && tsc -p ./", 11 | "watch": "npm run clear && tsc -watch -p ./", 12 | "__postinstall": "node ./node_modules/vscode/bin/install", 13 | "test": "npm run build && node ./node_modules/vscode/bin/test", 14 | "prepublish": "npm run build", 15 | "postpublish": "npm install && vsce publish" 16 | }, 17 | "engines": { 18 | "vscode": "^1.17.0" 19 | }, 20 | "categories": [ 21 | "Other" 22 | ], 23 | "keywords": [ 24 | "dtpl", 25 | "template", 26 | "file", 27 | "variable" 28 | ], 29 | "repository": { 30 | "type": "git", 31 | "url": "https://github.com/qiu8310/dot-template" 32 | }, 33 | "activationEvents": [ 34 | "*", 35 | "onLanguage:dtpl", 36 | "onCommand:dot-template-vscode.createTemplateFiles", 37 | "onCommand:dot-template-vscode.createRelatedFiles", 38 | "onCommand:dot-template-vscode.undoOrRedo" 39 | ], 40 | "main": "./out/vscode", 41 | "icon": "res/images/icon.png", 42 | "contributes": { 43 | "configuration": { 44 | "title": "dot-template-vscode", 45 | "type": "object", 46 | "properties": { 47 | "dot-template-vscode.debug": { 48 | "type": "boolean", 49 | "default": false, 50 | "description": "设置是否输出调试信息在项目根目录中的 dtpl.debug.log 文件中" 51 | }, 52 | "dot-template-vscode.noExampleWhenCreateDtplFolder": { 53 | "type": "boolean", 54 | "default": false, 55 | "description": "新建 .dtpl 文件夹时不要创建演示用的模板" 56 | }, 57 | "dot-template-vscode.watchFilesGlobPattern": { 58 | "type": "string", 59 | "default": "**/*", 60 | "description": "指定要监听的文件,使用了 minimatch 匹配,并开启了 dot=true,其它选项默认" 61 | }, 62 | "dot-template-vscode.commandInvalidTimeout": { 63 | "type": "number", 64 | "default": 60000, 65 | "description": "设置命令的有效时间,过期后就无法撤销或重新执行,单位毫秒" 66 | }, 67 | "dot-template-vscode.dtplFolderName": { 68 | "type": "string", 69 | "default": ".dtpl", 70 | "description": "文件夹的名称,用于存放模板文件及相关配置文件" 71 | }, 72 | "dot-template-vscode.dtplExtension": { 73 | "type": "string", 74 | "default": ".dtpl", 75 | "description": "指定 dtpl 模板文件的后缀名" 76 | }, 77 | "dot-template-vscode.ejsExtension": { 78 | "type": "string", 79 | "default": ".ejs", 80 | "description": "指定 ejs 模板文件的后缀名" 81 | }, 82 | "dot-template-vscode.njkExtension": { 83 | "type": "string", 84 | "default": ".njk", 85 | "description": "指定 nunjucks 模板文件的后缀名" 86 | }, 87 | "dot-template-vscode.minimatchOptions": { 88 | "type": "object", 89 | "default": { 90 | "matchBase": true, 91 | "nocomment": true, 92 | "dot": true 93 | }, 94 | "description": "minimatch 的选项,用于匹配模板名称, 参考:https://github.com/isaacs/minimatch#options", 95 | "properties": { 96 | "nobrace": { 97 | "type": "boolean", 98 | "description": "Do not expand {a,b} and {1..3} brace sets" 99 | }, 100 | "noglobstar": { 101 | "type": "boolean", 102 | "description": "Disable ** matching against multiple folder names" 103 | }, 104 | "dot": { 105 | "type": "boolean", 106 | "description": "Allow patterns to match filenames starting with a period, even if the pattern does not explicitly have a period in that spot" 107 | }, 108 | "noext": { 109 | "type": "boolean", 110 | "description": "Disable 'extglob style patterns like +(a|b)" 111 | }, 112 | "nocase": { 113 | "type": "boolean", 114 | "description": "Perform a case-insensitive match" 115 | }, 116 | "matchBase": { 117 | "type": "boolean", 118 | "description": "If set, then patterns without slashes will be matched against the basename of the path if it contains slashes" 119 | }, 120 | "nocomment": { 121 | "type": "boolean", 122 | "description": "Suppress the behavior of treating # at the start of a pattern as a comment" 123 | }, 124 | "nonegate": { 125 | "type": "boolean", 126 | "description": "Suppress the behavior of treating a leading ! character as negation" 127 | }, 128 | "flipNegate": { 129 | "type": "boolean", 130 | "description": "Returns from negate expressions the same as if they were not negated. (Ie, true on a hit, false on a miss.)" 131 | } 132 | } 133 | } 134 | } 135 | }, 136 | "commands": [ 137 | { 138 | "command": "dot-template-vscode.createTemplateFiles", 139 | "title": "DTPL: Create template files" 140 | }, 141 | { 142 | "command": "dot-template-vscode.createRelatedFiles", 143 | "title": "DTPL: Create related files" 144 | }, 145 | { 146 | "command": "dot-template-vscode.undoOrRedo", 147 | "title": "DTPL: Undo or Redo last action" 148 | } 149 | ], 150 | "keybindings": [ 151 | { 152 | "command": "dot-template-vscode.createTemplateFiles", 153 | "key": "ctrl+k ctrl+p", 154 | "mac": "cmd+k cmd+p" 155 | }, 156 | { 157 | "command": "dot-template-vscode.createRelatedFiles", 158 | "key": "ctrl+k ctrl+s", 159 | "mac": "cmd+k cmd+s" 160 | }, 161 | { 162 | "command": "dot-template-vscode.undoOrRedo", 163 | "key": "ctrl+k ctrl+u", 164 | "mac": "cmd+k cmd+u" 165 | } 166 | ], 167 | "languages": [ 168 | { 169 | "id": "dtpl", 170 | "aliases": [ 171 | "Dot Template", 172 | "dtpl" 173 | ], 174 | "extensions": [ 175 | ".dtpl" 176 | ], 177 | "configuration": "./res/language-configuration.json" 178 | } 179 | ], 180 | "grammars": [ 181 | { 182 | "language": "dtpl", 183 | "scopeName": "source.dtpl", 184 | "path": "./res/syntaxes/dtpl.tmLanguage.json" 185 | } 186 | ], 187 | "snippets": [ 188 | { 189 | "path": "./res/snippets/inject-docs.json", 190 | "language": "javascript" 191 | }, 192 | { 193 | "path": "./res/snippets/inject-docs.json", 194 | "language": "typescript" 195 | }, 196 | { 197 | "path": "./res/snippets/inject-docs.json", 198 | "language": "javascriptreact" 199 | }, 200 | { 201 | "path": "./res/snippets/inject-docs.json", 202 | "language": "typescriptreact" 203 | }, 204 | { 205 | "path": "./res/snippets/inject-docs.json", 206 | "language": "css" 207 | }, 208 | { 209 | "path": "./res/snippets/inject-docs.json", 210 | "language": "scss" 211 | }, 212 | { 213 | "path": "./res/snippets/inject-docs.json", 214 | "language": "json" 215 | }, 216 | { 217 | "path": "./res/snippets/inject-hash.json", 218 | "language": "ignore" 219 | }, 220 | { 221 | "path": "./res/snippets/inject-hash.json", 222 | "language": "shellscript" 223 | }, 224 | { 225 | "path": "./res/snippets/inject-html.json", 226 | "language": "html" 227 | }, 228 | { 229 | "path": "./res/snippets/inject-html.json", 230 | "language": "markdown" 231 | } 232 | ] 233 | }, 234 | "devDependencies": { 235 | "@types/fs-extra": "~5.0.0", 236 | "@types/mocha": "~2.2.42", 237 | "@types/node": "^8.9.0", 238 | "mocha": "~5.0.0", 239 | "ts-node": "^4.1.0", 240 | "typescript": "^2.7.1", 241 | "vscode": "^1.1.5" 242 | }, 243 | "dependencies": { 244 | "dot-template-core": "^0.2.0", 245 | "fs-extra": "~5.0.0", 246 | "mora-scripts": "^1.6.28", 247 | "tslib": "^1.9.0" 248 | } 249 | } 250 | --------------------------------------------------------------------------------