Repository: wszgrcy/angular-miniprogram
Branch: master
Commit: 3a97f1f47f6f
Files: 497
Total size: 549.0 KB
Directory structure:
gitextract_1na6sspd/
├── .eslintignore
├── .eslintrc.json
├── .github/
│ └── workflows/
│ ├── alpha.yml
│ ├── default.yml
│ ├── deploy.yml
│ └── pull_request.yml
├── .gitignore
├── .husky/
│ ├── commit-msg
│ └── pre-commit
├── .npmrc
├── .nycrc.json
├── .prettierignore
├── .prettierrc
├── .vscode/
│ ├── launch.json
│ └── settings.json
├── LICENSE
├── commitlint.config.js
├── deploy/
│ └── doc/
│ ├── .gitignore
│ ├── Gemfile
│ ├── _config.yml
│ ├── _includes/
│ │ └── language-change.html
│ ├── _layouts/
│ │ ├── home.html
│ │ ├── post.html
│ │ └── redirect.html
│ ├── _posts/
│ │ ├── 2022-02-10-无时间测试.md
│ │ └── 2022-02-10-测试.md
│ ├── assets/
│ │ └── css/
│ │ └── style.scss
│ ├── en-US/
│ │ ├── attention.md
│ │ ├── index.md
│ │ ├── life-time.md
│ │ ├── miniprogram-feature.md
│ │ └── quick-start.md
│ ├── index.md
│ └── zh-Hans/
│ ├── attention.md
│ ├── index.md
│ ├── life-time.md
│ ├── miniprogram-feature.md
│ └── quick-start.md
├── jasmine.json
├── package.json
├── readme.md
├── script/
│ ├── build-ng-package.ts
│ ├── build.ts
│ ├── coverage-badge.ts
│ ├── package-sync.ts
│ ├── registry-transformer.js
│ ├── schema-merge.ts
│ ├── start-build-library.js
│ ├── startup-jasmine.ts
│ ├── tsconfig.json
│ └── tsconfig.startup-jasmine.json
├── src/
│ ├── builder/
│ │ ├── angular-internal/
│ │ │ ├── ast.type.ts
│ │ │ ├── selector.ts
│ │ │ ├── tags.ts
│ │ │ ├── template.ts
│ │ │ └── util.ts
│ │ ├── application/
│ │ │ ├── const.ts
│ │ │ ├── index.ts
│ │ │ ├── library-template-scope.service.ts
│ │ │ ├── loader/
│ │ │ │ ├── component-template.loader.ts
│ │ │ │ ├── library-template.loader.ts
│ │ │ │ ├── library.loader.ts
│ │ │ │ └── type.ts
│ │ │ ├── mini-program-application-analysis.service.ts
│ │ │ ├── plugin/
│ │ │ │ ├── dynamic-library-entry.plugin.ts
│ │ │ │ ├── dynamic-watch-entry.plugin.ts
│ │ │ │ └── export-mini-program-assets.plugin.ts
│ │ │ ├── schema.base.json
│ │ │ ├── schema.json
│ │ │ ├── token.ts
│ │ │ ├── type.ts
│ │ │ ├── util/
│ │ │ │ ├── index.ts
│ │ │ │ └── set-compilation-asset.ts
│ │ │ └── webpack-configuration-change.service.ts
│ │ ├── builder.prod.spec.ts
│ │ ├── builder.spec.ts
│ │ ├── builder.watch.spec.ts
│ │ ├── builders.json
│ │ ├── component-template-inject/
│ │ │ └── change-component.ts
│ │ ├── karma/
│ │ │ ├── client/
│ │ │ │ ├── adapter.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── karma.ts
│ │ │ │ ├── main.ts
│ │ │ │ ├── platform/
│ │ │ │ │ ├── index.ts
│ │ │ │ │ └── wx/
│ │ │ │ │ └── index.ts
│ │ │ │ ├── tsconfig.json
│ │ │ │ └── updater.ts
│ │ │ ├── index.origin.ts
│ │ │ ├── index.spec.ts
│ │ │ ├── index.ts
│ │ │ ├── plugin/
│ │ │ │ ├── index.js
│ │ │ │ ├── index.ts
│ │ │ │ ├── karma.ts
│ │ │ │ ├── launcher.ts
│ │ │ │ └── tsconfig.json
│ │ │ └── schema.json
│ │ ├── library/
│ │ │ ├── add-declaration-metadata.service.ts
│ │ │ ├── builder.ts
│ │ │ ├── compile-ngc.transform.ts
│ │ │ ├── compile-source-files.ts
│ │ │ ├── const.ts
│ │ │ ├── get-library-path.ts
│ │ │ ├── index.ts
│ │ │ ├── library.spec.ts
│ │ │ ├── merge-using-component-path.ts
│ │ │ ├── ng-packagr-factory.ts
│ │ │ ├── output-template-metadata.service.ts
│ │ │ ├── remove-publish-only.ts
│ │ │ ├── schema.json
│ │ │ ├── setup-component-data.service.ts
│ │ │ ├── stylesheet-processor.ts
│ │ │ ├── token.ts
│ │ │ └── type.ts
│ │ ├── mini-program-compiler/
│ │ │ ├── component-compiler.service.ts
│ │ │ ├── index.ts
│ │ │ ├── meta-collection.ts
│ │ │ ├── mini-program-compiler.service.ts
│ │ │ ├── parse-node/
│ │ │ │ ├── bound-text.ts
│ │ │ │ ├── component-context.ts
│ │ │ │ ├── content.ts
│ │ │ │ ├── element.ts
│ │ │ │ ├── index.ts
│ │ │ │ ├── interface.ts
│ │ │ │ ├── template-definition.ts
│ │ │ │ ├── template.ts
│ │ │ │ ├── text.ts
│ │ │ │ └── type.ts
│ │ │ └── type.ts
│ │ ├── platform/
│ │ │ ├── bd/
│ │ │ │ ├── bdzn-platform.ts
│ │ │ │ └── bdzn.transform.ts
│ │ │ ├── dd/
│ │ │ │ ├── dd-platform.ts
│ │ │ │ └── dd.transform.ts
│ │ │ ├── index.ts
│ │ │ ├── jd/
│ │ │ │ ├── jd-platform.ts
│ │ │ │ └── jd.transform.ts
│ │ │ ├── library/
│ │ │ │ ├── library-platform.ts
│ │ │ │ └── library.transform.ts
│ │ │ ├── platform-inject-config.ts
│ │ │ ├── platform.ts
│ │ │ ├── qq/
│ │ │ │ ├── qq-platform.ts
│ │ │ │ └── qq.transform.ts
│ │ │ ├── template/
│ │ │ │ └── app-template.js
│ │ │ ├── template-transform-strategy/
│ │ │ │ ├── transform.base.ts
│ │ │ │ └── wx-like/
│ │ │ │ ├── wx-container.ts
│ │ │ │ └── wx-transform.base.ts
│ │ │ ├── type.ts
│ │ │ ├── util/
│ │ │ │ ├── dataset-bind.ts
│ │ │ │ └── type-predicate.ts
│ │ │ ├── wx/
│ │ │ │ ├── wx-platform.ts
│ │ │ │ └── wx.transform.ts
│ │ │ ├── zfb/
│ │ │ │ ├── zfb-platform.ts
│ │ │ │ └── zfb.transform.ts
│ │ │ └── zjtd/
│ │ │ ├── zj-platform.ts
│ │ │ └── zj.transform.ts
│ │ ├── test/
│ │ │ └── fixture/
│ │ │ └── watch/
│ │ │ └── sub3/
│ │ │ ├── sub3.component.html
│ │ │ ├── sub3.component.ts
│ │ │ ├── sub3.entry.ts
│ │ │ └── sub3.module.ts
│ │ ├── token/
│ │ │ └── component.token.ts
│ │ └── util/
│ │ ├── index.ts
│ │ ├── library-template-scope-name.ts
│ │ ├── literal-resolve.ts
│ │ ├── load_esm.ts
│ │ ├── raw-updater.spec.ts
│ │ ├── raw-updater.ts
│ │ └── run-script.ts
│ └── library/
│ ├── common/
│ │ ├── .gitignore
│ │ └── ng-package.json
│ ├── declaration/
│ │ └── index.d.ts
│ ├── forms/
│ │ ├── .gitignore
│ │ ├── ng-package.json
│ │ ├── readme.md
│ │ └── src/
│ │ ├── directives/
│ │ │ ├── checkbox_value_accessor.ts
│ │ │ ├── default_value_accessor.ts
│ │ │ ├── picker_value_accessor.ts
│ │ │ ├── picker_view_value_accessor.ts
│ │ │ ├── radio_control_value_accessor.ts
│ │ │ ├── slider_value_accessor.ts
│ │ │ └── switch_value_accessor.ts
│ │ ├── directives.ts
│ │ └── forms.ts
│ ├── index.ts
│ ├── ng-package.json
│ ├── package.json
│ └── platform/
│ ├── bd/
│ │ ├── index.ts
│ │ ├── ng-package.json
│ │ └── platform-core.ts
│ ├── dd/
│ │ ├── index.ts
│ │ ├── ng-package.json
│ │ └── platform-core.ts
│ ├── default/
│ │ ├── agent-node.spec.ts
│ │ ├── agent-node.ts
│ │ ├── component-finder.service.ts
│ │ ├── component-template-hook.factory.ts
│ │ ├── diff-node-data.spec.ts
│ │ ├── diff-node-data.ts
│ │ ├── index.ts
│ │ ├── mini-program.renderer.factory.ts
│ │ ├── mini-program.renderer.ts
│ │ ├── ng-package.json
│ │ ├── platform-core.ts
│ │ └── token.ts
│ ├── http/
│ │ ├── README.md
│ │ ├── backend.ts
│ │ ├── index.ts
│ │ ├── module.ts
│ │ ├── provider.ts
│ │ └── response.ts
│ ├── index.ts
│ ├── jd/
│ │ ├── index.ts
│ │ ├── ng-package.json
│ │ └── platform-core.ts
│ ├── mini-program.module.ts
│ ├── ng-package.json
│ ├── page.service.ts
│ ├── platform-miniprogram.ts
│ ├── qq/
│ │ ├── index.ts
│ │ ├── ng-package.json
│ │ └── platform-core.ts
│ ├── token.ts
│ ├── type/
│ │ ├── index.ts
│ │ ├── internal-type.ts
│ │ ├── ng-package.json
│ │ └── type.ts
│ ├── wx/
│ │ ├── index.ts
│ │ └── ng-package.json
│ ├── zfb/
│ │ ├── index.ts
│ │ ├── ng-package.json
│ │ └── platform-core.ts
│ └── zjtd/
│ ├── index.ts
│ ├── ng-package.json
│ └── platform-core.ts
├── test/
│ ├── hello-world-app/
│ │ ├── .browserslistrc
│ │ ├── .gitignore
│ │ ├── angular.json
│ │ ├── karma.conf.js
│ │ ├── projects/
│ │ │ └── test-library/
│ │ │ ├── .browserslistrc
│ │ │ ├── ng-package.json
│ │ │ ├── package.json
│ │ │ ├── src/
│ │ │ │ ├── directive/
│ │ │ │ │ ├── directive.module.ts
│ │ │ │ │ └── input-output.directive.ts
│ │ │ │ ├── global-self-template/
│ │ │ │ │ ├── global-self-template.component.css
│ │ │ │ │ ├── global-self-template.component.html
│ │ │ │ │ ├── global-self-template.component.ts
│ │ │ │ │ └── global-self-template.module.ts
│ │ │ │ ├── lib/
│ │ │ │ │ ├── test-library.component.scss
│ │ │ │ │ ├── test-library.component.spec.ts
│ │ │ │ │ ├── test-library.component.ts
│ │ │ │ │ ├── test-library.directive.ts
│ │ │ │ │ ├── test-library.module.ts
│ │ │ │ │ ├── test-library.service.spec.ts
│ │ │ │ │ └── test-library.service.ts
│ │ │ │ ├── lib-comp1/
│ │ │ │ │ ├── lib-comp1.component.css
│ │ │ │ │ ├── lib-comp1.component.html
│ │ │ │ │ ├── lib-comp1.component.ts
│ │ │ │ │ ├── lib-comp1.module.ts
│ │ │ │ │ └── lib-dir1.directive.ts
│ │ │ │ ├── other/
│ │ │ │ │ ├── other.component.css
│ │ │ │ │ ├── other.component.html
│ │ │ │ │ ├── other.component.ts
│ │ │ │ │ └── other.module.ts
│ │ │ │ ├── outside-template/
│ │ │ │ │ ├── outside-template.component.css
│ │ │ │ │ ├── outside-template.component.html
│ │ │ │ │ ├── outside-template.component.ts
│ │ │ │ │ └── outside-template.module.ts
│ │ │ │ └── public-api.ts
│ │ │ ├── tsconfig.lib.json
│ │ │ ├── tsconfig.lib.prod.json
│ │ │ └── tsconfig.spec.json
│ │ ├── src/
│ │ │ ├── __components/
│ │ │ │ ├── component-need-template/
│ │ │ │ │ ├── component-need-template.component.css
│ │ │ │ │ ├── component-need-template.component.html
│ │ │ │ │ ├── component-need-template.component.ts
│ │ │ │ │ ├── component-need-template.entry.ts
│ │ │ │ │ └── component-need-template.module.ts
│ │ │ │ ├── component1/
│ │ │ │ │ ├── component1.component.css
│ │ │ │ │ ├── component1.component.html
│ │ │ │ │ ├── component1.component.ts
│ │ │ │ │ ├── component1.entry.ts
│ │ │ │ │ └── component1.module.ts
│ │ │ │ ├── component2/
│ │ │ │ │ ├── component2.component.css
│ │ │ │ │ ├── component2.component.html
│ │ │ │ │ ├── component2.component.ts
│ │ │ │ │ ├── component2.entry.ts
│ │ │ │ │ └── component2.module.ts
│ │ │ │ ├── component3/
│ │ │ │ │ ├── component3.component.css
│ │ │ │ │ ├── component3.component.html
│ │ │ │ │ ├── component3.component.ts
│ │ │ │ │ └── component3.entry.ts
│ │ │ │ ├── content/
│ │ │ │ │ ├── content.component.css
│ │ │ │ │ ├── content.component.html
│ │ │ │ │ ├── content.component.ts
│ │ │ │ │ ├── content.entry.ts
│ │ │ │ │ └── content.module.ts
│ │ │ │ ├── content-multi/
│ │ │ │ │ ├── content-multi.component.css
│ │ │ │ │ ├── content-multi.component.html
│ │ │ │ │ ├── content-multi.component.ts
│ │ │ │ │ ├── content-multi.entry.ts
│ │ │ │ │ └── content-multi.module.ts
│ │ │ │ └── life-time/
│ │ │ │ ├── life-time.component.html
│ │ │ │ ├── life-time.component.ts
│ │ │ │ └── life-time.entry.ts
│ │ │ ├── __pages/
│ │ │ │ ├── base-component/
│ │ │ │ │ ├── base-component.component.css
│ │ │ │ │ ├── base-component.component.html
│ │ │ │ │ ├── base-component.component.ts
│ │ │ │ │ ├── base-component.entry.ts
│ │ │ │ │ └── base-component.module.ts
│ │ │ │ ├── base-directive/
│ │ │ │ │ ├── base-directive.component.css
│ │ │ │ │ ├── base-directive.component.html
│ │ │ │ │ ├── base-directive.component.ts
│ │ │ │ │ ├── base-directive.entry.ts
│ │ │ │ │ ├── base-directive.module.ts
│ │ │ │ │ └── directive1.directive.ts
│ │ │ │ ├── base-forms/
│ │ │ │ │ ├── base-forms.component.css
│ │ │ │ │ ├── base-forms.component.html
│ │ │ │ │ ├── base-forms.component.ts
│ │ │ │ │ ├── base-forms.entry.ts
│ │ │ │ │ └── base-forms.module.ts
│ │ │ │ ├── base-http/
│ │ │ │ │ ├── base-http.component.css
│ │ │ │ │ ├── base-http.component.html
│ │ │ │ │ ├── base-http.component.ts
│ │ │ │ │ ├── base-http.entry.ts
│ │ │ │ │ └── base-http.module.ts
│ │ │ │ ├── base-tap/
│ │ │ │ │ ├── base-tap.component.css
│ │ │ │ │ ├── base-tap.component.html
│ │ │ │ │ ├── base-tap.component.ts
│ │ │ │ │ ├── base-tap.entry.ts
│ │ │ │ │ └── base-tap.module.ts
│ │ │ │ ├── complex-property-event/
│ │ │ │ │ ├── app-dir1.directive.ts
│ │ │ │ │ ├── complex-property-event.component.css
│ │ │ │ │ ├── complex-property-event.component.html
│ │ │ │ │ ├── complex-property-event.component.ts
│ │ │ │ │ ├── complex-property-event.entry.ts
│ │ │ │ │ └── complex-property-event.module.ts
│ │ │ │ ├── complex-structure/
│ │ │ │ │ ├── complex-structure.component.css
│ │ │ │ │ ├── complex-structure.component.html
│ │ │ │ │ ├── complex-structure.component.ts
│ │ │ │ │ ├── complex-structure.entry.ts
│ │ │ │ │ └── complex-structure.module.ts
│ │ │ │ ├── component-use-template/
│ │ │ │ │ ├── component-use-template.component.css
│ │ │ │ │ ├── component-use-template.component.html
│ │ │ │ │ ├── component-use-template.component.ts
│ │ │ │ │ ├── component-use-template.entry.ts
│ │ │ │ │ └── component-use-template.module.ts
│ │ │ │ ├── custom-structural-directive/
│ │ │ │ │ ├── custom-structural-directive.component.css
│ │ │ │ │ ├── custom-structural-directive.component.html
│ │ │ │ │ ├── custom-structural-directive.component.ts
│ │ │ │ │ ├── custom-structural-directive.entry.ts
│ │ │ │ │ ├── custom-structural-directive.module.ts
│ │ │ │ │ └── structural1.directive.ts
│ │ │ │ ├── default-structural-directive/
│ │ │ │ │ ├── default-structural-directive.component.css
│ │ │ │ │ ├── default-structural-directive.component.html
│ │ │ │ │ ├── default-structural-directive.component.ts
│ │ │ │ │ ├── default-structural-directive.entry.ts
│ │ │ │ │ └── default-structural-directive.module.ts
│ │ │ │ ├── life-time-page/
│ │ │ │ │ ├── life-time-page.entry.ts
│ │ │ │ │ ├── life-time.component.html
│ │ │ │ │ ├── life-time.component.ts
│ │ │ │ │ └── life-time.module.ts
│ │ │ │ ├── life-time-page-use-component/
│ │ │ │ │ ├── life-time-page-use-component.entry.ts
│ │ │ │ │ ├── life-time.component.html
│ │ │ │ │ ├── life-time.component.ts
│ │ │ │ │ └── life-time.module.ts
│ │ │ │ ├── ng-content/
│ │ │ │ │ ├── ng-content.component.css
│ │ │ │ │ ├── ng-content.component.html
│ │ │ │ │ ├── ng-content.component.ts
│ │ │ │ │ ├── ng-content.entry.ts
│ │ │ │ │ └── ng-content.module.ts
│ │ │ │ ├── root/
│ │ │ │ │ ├── root.component.css
│ │ │ │ │ ├── root.component.html
│ │ │ │ │ ├── root.component.ts
│ │ │ │ │ ├── root.entry.ts
│ │ │ │ │ └── root.module.ts
│ │ │ │ └── self-component/
│ │ │ │ ├── self-component.component.css
│ │ │ │ ├── self-component.component.html
│ │ │ │ ├── self-component.component.ts
│ │ │ │ ├── self-component.entry.ts
│ │ │ │ └── self-component.module.ts
│ │ │ ├── app.json
│ │ │ ├── assets/
│ │ │ │ └── .gitkeep
│ │ │ ├── environments/
│ │ │ │ ├── environment.prod.ts
│ │ │ │ └── environment.ts
│ │ │ ├── main-test.module.ts
│ │ │ ├── main.module.ts
│ │ │ ├── main.ts
│ │ │ ├── project.config.json
│ │ │ ├── spec/
│ │ │ │ ├── empty/
│ │ │ │ │ ├── empty.component.ts
│ │ │ │ │ ├── empty.entry.ts
│ │ │ │ │ └── empty.module.ts
│ │ │ │ ├── http-spec/
│ │ │ │ │ ├── http-spec.entry.ts
│ │ │ │ │ ├── http.component.ts
│ │ │ │ │ ├── http.module.ts
│ │ │ │ │ └── http.spec.ts
│ │ │ │ ├── life-time-spec/
│ │ │ │ │ ├── life-time-spec.entry.ts
│ │ │ │ │ ├── life-time.component.ts
│ │ │ │ │ ├── life-time.module.ts
│ │ │ │ │ └── life-time.spec.ts
│ │ │ │ ├── ng-content-spec/
│ │ │ │ │ ├── ng-content-spec.entry.ts
│ │ │ │ │ ├── ng-content.component.ts
│ │ │ │ │ ├── ng-content.module.ts
│ │ │ │ │ └── ng-content.spec.ts
│ │ │ │ ├── ng-for-spec/
│ │ │ │ │ ├── ng-for-spec.entry.ts
│ │ │ │ │ ├── ng-for.component.ts
│ │ │ │ │ ├── ng-for.module.ts
│ │ │ │ │ └── ng-for.spec.ts
│ │ │ │ ├── ng-if-spec/
│ │ │ │ │ ├── ng-if-spec.entry.ts
│ │ │ │ │ ├── ng-if.component.ts
│ │ │ │ │ ├── ng-if.module.ts
│ │ │ │ │ └── ng-if.spec.ts
│ │ │ │ ├── ng-library-import-spec/
│ │ │ │ │ ├── ng-library-import-spec.entry.ts
│ │ │ │ │ ├── ng-library-import.component.ts
│ │ │ │ │ ├── ng-library-import.module.ts
│ │ │ │ │ └── ng-library-import.spec.ts
│ │ │ │ ├── ng-switch-spec/
│ │ │ │ │ ├── ng-switch-spec.entry.ts
│ │ │ │ │ ├── ng-switch.component.ts
│ │ │ │ │ ├── ng-switch.module.ts
│ │ │ │ │ └── ng-switch.spec.ts
│ │ │ │ ├── ng-template-outlet-spec/
│ │ │ │ │ ├── ng-template-outlet-spec.entry.ts
│ │ │ │ │ ├── ng-template-outlet.component.ts
│ │ │ │ │ ├── ng-template-outlet.module.ts
│ │ │ │ │ └── ng-template-outlet.spec.ts
│ │ │ │ ├── self-template-spec/
│ │ │ │ │ ├── self-template-spec.entry.ts
│ │ │ │ │ ├── self-template.component.ts
│ │ │ │ │ ├── self-template.module.ts
│ │ │ │ │ └── self-template.spec.ts
│ │ │ │ ├── style-class-spec/
│ │ │ │ │ ├── style-class-spec.component.ts
│ │ │ │ │ ├── style-class-spec.entry.ts
│ │ │ │ │ ├── style-class-spec.module.ts
│ │ │ │ │ └── style-class-spec.spec.ts
│ │ │ │ ├── tag-view-convert-spec/
│ │ │ │ │ ├── tag-view-convert-spec.entry.ts
│ │ │ │ │ ├── tag-view-convert.component.ts
│ │ │ │ │ ├── tag-view-convert.module.ts
│ │ │ │ │ └── tag-view-convert.spec.ts
│ │ │ │ └── util/
│ │ │ │ ├── index.ts
│ │ │ │ ├── node-query.ts
│ │ │ │ ├── open-component.ts
│ │ │ │ └── page-info.ts
│ │ │ ├── spec-component/
│ │ │ │ ├── life-time/
│ │ │ │ │ ├── life-time.component.ts
│ │ │ │ │ ├── life-time.entry.ts
│ │ │ │ │ └── life-time.module.ts
│ │ │ │ ├── ng-content/
│ │ │ │ │ ├── ng-content.component.html
│ │ │ │ │ ├── ng-content.component.ts
│ │ │ │ │ ├── ng-content.entry.ts
│ │ │ │ │ └── ng-content.module.ts
│ │ │ │ ├── ng-for/
│ │ │ │ │ ├── ng-for.component.html
│ │ │ │ │ ├── ng-for.component.ts
│ │ │ │ │ ├── ng-for.entry.ts
│ │ │ │ │ └── ng-for.module.ts
│ │ │ │ ├── ng-if/
│ │ │ │ │ ├── ng-if.component.html
│ │ │ │ │ ├── ng-if.component.ts
│ │ │ │ │ ├── ng-if.entry.ts
│ │ │ │ │ └── ng-if.module.ts
│ │ │ │ ├── ng-library-import/
│ │ │ │ │ ├── ng-library-import.component.html
│ │ │ │ │ ├── ng-library-import.component.ts
│ │ │ │ │ ├── ng-library-import.entry.ts
│ │ │ │ │ └── ng-library-import.module.ts
│ │ │ │ ├── ng-switch/
│ │ │ │ │ ├── ng-switch.component.html
│ │ │ │ │ ├── ng-switch.component.ts
│ │ │ │ │ ├── ng-switch.entry.ts
│ │ │ │ │ └── ng-switch.module.ts
│ │ │ │ ├── ng-template-outlet/
│ │ │ │ │ ├── ng-template-outlet.component.html
│ │ │ │ │ ├── ng-template-outlet.component.ts
│ │ │ │ │ ├── ng-template-outlet.entry.ts
│ │ │ │ │ └── ng-template-outlet.module.ts
│ │ │ │ ├── self-template/
│ │ │ │ │ ├── self-template.component.html
│ │ │ │ │ ├── self-template.component.ts
│ │ │ │ │ ├── self-template.entry.ts
│ │ │ │ │ └── self-template.module.ts
│ │ │ │ ├── style-class/
│ │ │ │ │ ├── style-class.component.html
│ │ │ │ │ ├── style-class.component.ts
│ │ │ │ │ ├── style-class.entry.ts
│ │ │ │ │ └── style-class.module.ts
│ │ │ │ └── tag-view-convert/
│ │ │ │ ├── tag-view-convert.component.html
│ │ │ │ ├── tag-view-convert.component.ts
│ │ │ │ ├── tag-view-convert.entry.ts
│ │ │ │ └── tag-view-convert.module.ts
│ │ │ ├── styles.css
│ │ │ ├── test.ts
│ │ │ ├── tsconfig.app.json
│ │ │ ├── tsconfig.dev.json
│ │ │ ├── tsconfig.spec.json
│ │ │ └── typings.d.ts
│ │ ├── tsconfig.base.json
│ │ └── tsconfig.json
│ ├── plugin-describe-builder/
│ │ └── index.ts
│ ├── test-builder/
│ │ ├── index.ts
│ │ ├── schema.json
│ │ ├── schema.karma.json
│ │ └── schema.library.json
│ └── util/
│ └── file.ts
├── tsconfig.base.json
├── tsconfig.builder.json
├── tsconfig.doc.json
├── tsconfig.internal-schematics.json
├── tsconfig.json
├── tsconfig.library.json
├── tsconfig.spec.json
└── typedoc.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .eslintignore
================================================
/tests/
.yarn/
dist/
node_modules/
test/test-project-host-hello-world-app-*
test/hello-world-app
jest.builder.config.ts
build-ng-package.ts
schema-merge.ts
test/util
commitlint.config.js
script
**/fixture
*.xspec.ts
src/library/common
src/libary/forms
test
src/library/platform/http
deploy
docs
================================================
FILE: .eslintrc.json
================================================
{
"ignorePatterns": [
"src/library/forms/**/*",
"src/library/common/**/*",
"*.d.ts",
"src/builder/karma/client/**/*",
"src/builder/karma/plugin/**/*"
],
"root": true,
"env": {
"es6": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:import/typescript",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "tsconfig.json",
"sourceType": "module"
},
"plugins": ["eslint-plugin-import", "@typescript-eslint"],
"rules": {
"@typescript-eslint/consistent-type-assertions": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/no-unnecessary-qualifier": "warn",
"@typescript-eslint/no-unused-expressions": "warn",
"curly": "warn",
"import/first": "warn",
"import/newline-after-import": "warn",
"import/no-absolute-path": "warn",
"import/no-duplicates": "warn",
"import/no-extraneous-dependencies": [
"off",
{
"devDependencies": false
}
],
"import/no-unassigned-import": [
"warn",
{
"allow": ["miniprogram-api-typings"]
}
],
"import/order": [
"warn",
{
"alphabetize": {
"order": "asc"
},
"groups": [["builtin", "external"], "parent", "sibling", "index"]
}
],
"max-len": [
"warn",
{
"code": 140,
"ignoreUrls": true,
"ignoreStrings": true,
"ignoreTemplateLiterals": true,
"ignoreComments": true,
"ignoreRegExpLiterals": true
}
],
"max-lines-per-function": [
"warn",
{
"max": 400
}
],
"no-caller": "warn",
"no-console": "warn",
"no-empty": [
"warn",
{
"allowEmptyCatch": true
}
],
"no-eval": "warn",
"no-multiple-empty-lines": ["warn"],
"no-throw-literal": "warn",
"no-var": "warn",
"sort-imports": [
"warn",
{
"ignoreDeclarationSort": true
}
],
"spaced-comment": [
"warn",
"always",
{
"markers": ["/"]
}
],
/* TODO: evaluate usage of these rules and fix issues as needed */
"no-case-declarations": "off",
"no-fallthrough": "off",
"no-underscore-dangle": "off",
"@typescript-eslint/await-thenable": "off",
"@typescript-eslint/ban-types": "off",
"@typescript-eslint/no-empty-function": "off",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-implied-eval": "off",
"@typescript-eslint/no-var-requires": "off",
"@typescript-eslint/no-unnecessary-type-assertion": "off",
"@typescript-eslint/no-unsafe-assignment": "off",
"@typescript-eslint/no-unsafe-call": "off",
"@typescript-eslint/no-unsafe-member-access": "off",
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/no-unused-vars": "off",
"@typescript-eslint/prefer-regexp-exec": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/restrict-plus-operands": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/unbound-method": "off",
"@typescript-eslint/no-unsafe-argument": "off",
"prefer-const": "warn",
"prefer-rest-params": "warn",
"prefer-spread": "warn",
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-inferrable-types": "off",
"@typescript-eslint/no-misused-promises": [
"warn",
{
"checksVoidReturn": false
}
],
"@typescript-eslint/no-this-alias": [
"warn",
{
"allowedNames": ["_this"]
}
]
},
"overrides": [
{
"files": ["**/*.spec.ts"],
"parserOptions": {
"project": "tsconfig.spec.json"
},
"rules": {
"import/no-extraneous-dependencies": [
"warn",
{
"devDependencies": true,
"packageDir": "./"
}
],
"max-lines-per-function": "off",
"no-console": "off"
}
},
{
"files": ["./src/builder/**/*.ts"],
"excludedFiles": [
"./src/**/*.template.ts",
"./src/**/*.d.ts",
"**/*.spec.ts",
"test"
],
"parserOptions": {
"project": "./tsconfig.builder.json"
}
},
{
"files": ["./src/library/**/*.ts"],
"excludedFiles": [
"./src/**/*.template.ts",
"./src/**/*.d.ts",
"**/*.spec.ts",
"test",
"./src/library/forms/**/*",
"./src/library/common/**/*",
"./src/library/platform/http/**/*"
],
"parserOptions": {
"project": "tsconfig.library.json"
}
},
{
"files": ["schematics/internal/**/*.ts"],
"excludedFiles": [],
"parserOptions": {
"project": "tsconfig.internal-schematics.json"
}
}
]
}
================================================
FILE: .github/workflows/alpha.yml
================================================
name: CI
on:
push:
branches:
- alpha
env:
REPOSITORY_PATH: https://${{secrets.ACCESS_TOKEN}}@github.com/
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: init
run: |
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@gmail.com"
- name: pull-code
uses: actions/checkout@v2
- name: install-node
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: "npm"
- name: install-dependencies
run: |
npm ci --legacy-peer-deps
- name: lint
run: |
npm run lint
- name: hook-code
run: |
npm run sync
- name: test
run: |
npm run test:ci
- name: build
run: |
npm run build
- id: publish
name: publish
uses: JS-DevTools/npm-publish@v3
if: ${{ github.repository_owner == 'wszgrcy' }}
with:
token: ${{ secrets.NPM_PUBLISH_TOKEN }}
package: ./dist/package.json
tag: alpha
- if: ${{ github.repository_owner == 'wszgrcy' && steps.publish.outputs.type }}
run: |
echo "[${{ steps.publish.outputs.type }}]版本已变更: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}"
git tag v${{steps.publish.outputs.version}}
git push origin v${{steps.publish.outputs.version}}
================================================
FILE: .github/workflows/default.yml
================================================
name: CI
on:
push:
branches:
- master
env:
REPOSITORY_PATH: https://${{secrets.ACCESS_TOKEN}}@github.com/
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: init
run: |
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@gmail.com"
- name: pull-code
uses: actions/checkout@v2
- name: install-node
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: "npm"
- name: install-dependencies
run: |
npm ci --legacy-peer-deps
- name: lint
run: |
npm run lint
- name: hook-code
run: |
npm run sync
- name: test
run: |
npm run test:ci
- name: build
run: |
npm run build
- id: publish
name: publish
uses: JS-DevTools/npm-publish@v1
if: ${{ github.repository_owner == 'wszgrcy' }}
with:
token: ${{ secrets.NPM_PUBLISH_TOKEN }}
package: ./dist/package.json
- if: ${{ github.repository_owner == 'wszgrcy' && steps.publish.outputs.type }}
run: |
echo "[${{ steps.publish.outputs.type }}]Version changed: ${{ steps.publish.outputs.old-version }} => ${{ steps.publish.outputs.version }}"
git tag v${{steps.publish.outputs.version}}
git push origin v${{steps.publish.outputs.version}}
================================================
FILE: .github/workflows/deploy.yml
================================================
name: deploy
on:
push:
branches:
- master
env:
REPOSITORY_PATH: https://${{secrets.ACCESS_TOKEN}}@github.com/
# GITHUB_TOKEN: ${{secrets.ACCESS_TOKEN}}
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: init
run: |
git config --global user.name "${GITHUB_ACTOR}"
git config --global user.email "${GITHUB_ACTOR}@gmail.com"
- name: pull-code
uses: actions/checkout@v2
- name: install-node
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: "npm"
- name: install-dependencies
run: |
npm ci --legacy-peer-deps
- name: hook-code
run: |
npm run sync
- name: build-docs
continue-on-error: true
run: |
npm run deploy
cp -rp docs/ ../dist
git branch gh-pages
git checkout gh-pages
rm -rf *
cp -rp ../dist/* .
cp ./index.html 404.html
env:
INPUT_TOKEN: ''
- name: tag
run: |
git status
git add -A
HUSKY=0 git commit -m 'build: 页面构建'
HUSKY=0 git push --force "${REPOSITORY_PATH}${GITHUB_REPOSITORY}.git" gh-pages
# git pull
================================================
FILE: .github/workflows/pull_request.yml
================================================
name: PR
on:
pull_request:
branches:
- master
- alpha
jobs:
pr-test:
runs-on: ubuntu-latest
steps:
- name: pull-code
uses: actions/checkout@v2
- name: install-node
uses: actions/setup-node@v2
with:
node-version: 16.x
cache: "npm"
- name: install-dependencies
run: |
npm ci --legacy-peer-deps
- name: lint
run: |
npm run lint
- name: hook-code
run: |
npm run sync
- name: test
run: |
npm run test:ci
- name: build
run: |
npm run build
================================================
FILE: .gitignore
================================================
node_modules*
test-project-host-hello-world-app-*
dist
__test-app
coverage-builder
script/startup-jasmine.js
.temp-git
.nyc_output
docs
deploy/api-doc
================================================
FILE: .husky/commit-msg
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install commitlint --edit "$1"
================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
npx --no-install pretty-quick --staged
================================================
FILE: .npmrc
================================================
# registry =https://registry.npmmirror.com/
================================================
FILE: .nycrc.json
================================================
{
"extension": [".ts"],
"exclude": [
"**/*.d.ts",
"**/*.js",
"**/*.spec.ts",
"startup-jasmine.js",
"test/**/*",
"src/builder/angular-internal/**/*"
],
"reporter": ["html", "text", "json-summary", "json"],
"all": false,
"report-dir": "./docs/coverage"
}
================================================
FILE: .prettierignore
================================================
deploy
================================================
FILE: .prettierrc
================================================
{
"singleQuote": true
}
================================================
FILE: .vscode/launch.json
================================================
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "测试(jasmine)-library",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "test", "library"],
"port": 7899
},
{
"name": "测试(jasmine)-browser",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "test", "builder-dev"],
"port": 7899
},
{
"name": "测试(jasmine)-builder-prod",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "test", "builder-prod"],
"port": 7899
},
{
"name": "测试(jasmine)-browser-watch",
"type": "node",
"request": "launch",
"cwd": "${workspaceRoot}",
"runtimeExecutable": "npm",
"runtimeArgs": ["run-script", "test", "watch"],
"port": 7899
}
]
}
================================================
FILE: .vscode/settings.json
================================================
{
"typescript.tsdk": "node_modules/typescript/lib",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit",
"source.fixAll.tslint": "never"
}
}
================================================
FILE: LICENSE
================================================
MIT License
Copyright (c) 2021 chen
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
================================================
FILE: commitlint.config.js
================================================
module.exports = {
extends: ['@commitlint/config-conventional'],
};
================================================
FILE: deploy/doc/.gitignore
================================================
_site
.jekyll-cache
================================================
FILE: deploy/doc/Gemfile
================================================
source "https://mirrors.tuna.tsinghua.edu.cn/rubygems/"
# Hello! This is where you manage which Jekyll version is used to run.
# When you want to use a different version, change it below, save the
# file and run `bundle install`. Run Jekyll with `bundle exec`, like so:
#
# bundle exec jekyll serve
#
# This will help ensure the proper Jekyll version is running.
# Happy Jekylling!
#gem "jekyll", "~> 4.2.2"
# This is the default theme for new Jekyll sites. You may change this to anything you like.
gem "minima", "~> 2.5"
# If you want to use GitHub Pages, remove the "gem "jekyll"" above and
# uncomment the line below. To upgrade, run `bundle update github-pages`.
gem "github-pages", "~> 226", group: :jekyll_plugins
# If you have any plugins, put them here!
group :jekyll_plugins do
gem "jekyll-feed", "~> 0.12"
end
# Windows and JRuby does not include zoneinfo files, so bundle the tzinfo-data gem
# and associated library.
platforms :mingw, :x64_mingw, :mswin, :jruby do
gem "tzinfo", "~> 1.2"
gem "tzinfo-data"
end
# Performance-booster for watching directories on Windows
gem "wdm", "~> 0.1.1", :platforms => [:mingw, :x64_mingw, :mswin]
# Lock `http_parser.rb` gem to `v0.6.x` on JRuby builds since newer versions of the gem
# do not have a Java counterpart.
gem "http_parser.rb", "~> 0.6.0", :platforms => [:jruby]
================================================
FILE: deploy/doc/_config.yml
================================================
theme: jekyll-theme-tactile
baseurl: /angular-miniprogram
================================================
FILE: deploy/doc/_includes/language-change.html
================================================
================================================
FILE: deploy/doc/_layouts/home.html
================================================
{% seo %}
{% include head-custom.html %}
{% include language-change.html %}
{{ page.title | default: site.title | default: site.github.repository_name }}
================================================
FILE: deploy/doc/_layouts/post.html
================================================
{% seo %}
{% include head-custom.html %}
Home
{% include language-change.html %}
{{ page.title | default: site.title | default: site.github.repository_name }}
================================================
FILE: deploy/doc/_layouts/redirect.html
================================================
Document
================================================
FILE: deploy/doc/_posts/2022-02-10-无时间测试.md
================================================
---
title: "无时间测试"
layout: post
---
# 内容
- 没有
================================================
FILE: deploy/doc/_posts/2022-02-10-测试.md
================================================
---
title: "测试"
layout: post
---
# 内容
================================================
FILE: deploy/doc/assets/css/style.scss
================================================
---
---
@import '{{ site.theme }}';
.action-line {
display: flex;
align-items: center;
justify-content: end;
}
.flex-1 {
flex: 1 1 0%;
}
.flex {
display: flex;
}
.text-center {
text-align: center;
}
blockquote p{
font-size: 0.8em;
}
================================================
FILE: deploy/doc/en-US/attention.md
================================================
---
layout: post
title: Attention
---
## Forbidden
- All Dom operation
## Import Change
- Use `angular-miniprogram/common` replace `@angular/common`
- Use `angular-miniprogram/common/http` replace `@angular/common/http`
- Use `angular-miniprogram/forms` replace `@angular/forms`
- Use `import { HttpClientModule, provideHttpClient } from 'angular-miniprogram'` replace `import { HttpClientModule, provideHttpClient } from '@angular/common/http'`
## Attention
- A event with prefix,like `bind`,`mut-bind`,etc,remove `:`,such as `bind:tap`=>`bindtap`
> Prefix in Angular resolve as `target`(window/document/body)
- Miniprogram native component need set `schemas:[NO_ERRORS_SCHEMA]` in `NgModule`
- if property operation not display on view, need to use `detectChanges`
- Now, one file only allow one `@Component`
## Compatible
### Use Template
- `createEmbeddedView` only allow in `structural directive`,or `TemplateRef`
- `createEmbeddedView`need a `__templateName` property in `context` object,this property is miniprogram name
- this name can be find in a private variable: `(this.templateRef as any)._declarationTContainer.localNames[0]`
- only first `template variable` name can be use
```ts
@Directive({
selector: '[appStructural1]',
})
export class Structural1Directive {
@Input() appStructural1: TemplateRef;
@Input() appStructural1Name: string;
constructor(private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
this.viewContainerRef.createEmbeddedView(this.appStructural1, {
__templateName: this.appStructural1Name,
});
}
}
```
### Template Rename
- `ng-template` name can't repeat in one Component, if exist, you can use mulit `template variable`
- ` ` `#name1` is the alias name, and `#name2` is the repeat name
- The first name will write in the template file, but all `template variable` can be use in Angular
### Cross Component use Template
- In same project, a Component use other Component `TemlateRef`,template name should follow this format`$$mp$$__self$$xxx`
> `same application`or`same library` is same project
```html
content
```
- transfer `TemplateRef` to Other library Component, template name should follow this format`$$mp$$TemplateScopeName$$xxx`, `TemplateScopeName`rule as follow:
```ts
import { strings } from '@angular-devkit/core';
//library library name
export function libraryTemplateScopeName(library: string) {
return strings.classify(library.replace(/[@/]/g, ''));
}
```
- for example: `test-library`=>`TestLibrary`,`@my/library`=>`MyLibrary`
```html
```
### Unrealized
- control flow
> This part of the functionality currently appears to be dynamically generated, and to convert it to static, we can only use some native methods of the mini program, but it cannot be matched 1:1, so we haven't thought of a good method yet
================================================
FILE: deploy/doc/en-US/index.md
================================================
---
title: 'angular-miniprogram'
layout: home
---
Develop MiniProgram using Angular
Use Angular Ecosystem as possible,
reduce cross-platform costs
## document
- [Quickstart](quick-start)
- [Attention](attention)
- [MiniProgramFeature](miniprogram-feature)
- [API](../api-doc)
- [life-time](life-time)
## Development Environment
### Prerequisites
- `Node.js` greater than 20
- `@angular/cli` 17
### hello-world
- use this Template [https://github.com/wszgrcy/angular-miniprogram-template](https://github.com/wszgrcy/angular-miniprogram-template) or download
- use `npm` install dependencies
### Quickstart
- [Quickstart](https://github.com/wszgrcy/angular-miniprogram/blob/master/quick-start-en.md)
## Ecosystem
- support all `Pipe` and `Service`
- compatible support `Directive`(a little different from web)
- one file only allow one `Component`
## Support Platform
| Platform Name | Run | Alias | comment |
| ------------- | --- | ----------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| Wechat | ✅ | Work Wechat | |
| bytedance | ✅ | | |
| jd | ❌ | | no account |
| baidu | ✅ | | |
| Alipay | ✅ | DingTalk,Alipay IoT,etc | can't compile in Prod, [see](https://forum.alipay.com/mini-app/post/65101060);can't use v2, while `slot` has some problem |
| qq | ✅ | | unknown error?but can work |
| feishu | ❌ | | like`bytedance`,but compile fail |
================================================
FILE: deploy/doc/en-US/life-time.md
================================================
---
layout: post
title: Life Time
---
# Component(Component)
- created
> Wait for link (miniprogram component is associated with ng component)
- properties Change(first)
> link(init with ng auto)
- attached
- ready
# Page(Page)
- onLoad
> init and link
- onShow(first)
- onReady
# Page(Component)
- onLoad
- onShow(first)
- created
> init
- attached
> link
- onReady
# Component(Component) in Page(Page)
- ng-constructor(onLoad start)
> ng page Component
> init and link
- ng-constructor(component)
> ng component Component
- ng-ngOnInit
- ng-ngAfterContentInit
- ng-ngOnInit(component)
- ng-ngAfterContentInit(component)
- ng-ngAfterViewInit(component)
- ng-ngAfterViewInit
- mp-created(component)
- mp-attached(component)
> sub component link(after created ,before created)
- mp-onLoad(onLoad end)
- mp-onShow
- mp-onReady
- mp-ready(component)
================================================
FILE: deploy/doc/en-US/miniprogram-feature.md
================================================
---
layout: post
title: Miniprogram Feature
---
## Service
- `ComponentFinderService`: get MiniProgram Component corresponding to Angular Component
```ts
import { ComponentFinderService } from 'angular-miniprogram';
//...
{
constructor(private componentFinderService: ComponentFinderService) {}
}
```
## token
- `APP_TOKEN`: get App instance
- `PAGE_TOKEN`: get MiniProgram Page corresponding to Angular Component
```ts
import {
APP_TOKEN,
PAGE_TOKEN,
} from 'angular-miniprogram';
//...
{
constructor(
@Inject(APP_TOKEN) appInstance: any,
@Inject(PAGE_TOKEN) pageInstance: any
) {}
}
```
## api
- `pageStartup`: page entry
```ts
import { pageStartup } from 'angular-miniprogram';
//...
pageStartup(Page1Module, Page1Component);
```
- `componentRegistry`: component registry
```ts
import { componentRegistry } from 'angular-miniprogram';
//...
componentRegistry(Component1Component);
```
## Static property on @Component
- `static mpPageOptions`: like `Page(mpPageOptions)`
> use when Component as a Page
```ts
@Component({
selector: 'app-life-time',
templateUrl: './life-time.component.html',
})
export class LifeTimePage implements OnInit {
static mpPageOptions: WechatMiniprogram.Page.Options<{}, {}> = {
onLoad: function (this: MiniProgramComponentInstance) {
console.log('mp-onLoad', this.__ngComponentInstance);
},
onShow: function () {
console.log('mp-onShow', this.__ngComponentInstance);
},
onReady: function (
this: WechatMiniprogram.Page.Instance<{}, {}> &
MiniProgramComponentInstance
) {
console.log('mp-onReady');
},
};
//...
}
```
- `static mpComponentOptions`: like `Component(mpComponentOptions)`
> use when Component as a Component or a Page but `pageStartup` with `{useComponent:true}`
```ts
@Component({
selector: 'app-life-time-component',
templateUrl: './life-time.component.html',
standalone: true,
})
export class LifeTimeComponent implements OnInit {
static mpComponentOptions: WechatMiniprogram.Component.Options<{}, {}, {}> = {
lifetimes: {
created: function (this: MiniProgramComponentInstance) {
console.log('created(component)');
},
attached: function () {
console.log('attached(component)');
},
ready: function () {
console.log('ready(component)');
},
moved: function () {
console.log('moved(component)');
},
detached: function () {
console.log('detached(component)');
},
error: function () {
console.log('error(component)');
},
},
pageLifetimes: {
show: function () {
console.log('page-show(component)');
},
hide: function () {
console.log('page-hide(component)');
},
resize: function () {
console.log('page-resize(component)');
},
},
};
//...
}
```
## Global Variable
- `miniProgramPlatform`: string, hint miniprogram platform(`wx`,`zfb`,`zj`,`bdzn`,`qq`,`dd`,etc)
================================================
FILE: deploy/doc/en-US/quick-start.md
================================================
---
layout: post
title: Quick Start
---
## set Page pattern
- Set output Page pattern in `angular.json`, same with `assets` field
```json
"pages": [
{
"glob": "**/*.entry.ts",
"input": "./src/pages",
"output": "pages"
}
]
```
- This config is, in `src/pages` dir match `*.entry.ts` file, and output in`[outputDir]/pages` Dir
## add a Page
- Create code with the following structure
```tree
├── page1.component.html
├── page1.component.scss
├── page1.component.ts
├── page1.entry.json
├── page1.entry.ts
└── page1.module.ts
```
- Create a `page1.entry.ts` file, add code as follow
```ts
import { pageStartup } from 'angular-miniprogram';
import { Page1Component } from './page1.component';
import { Page1Module } from './page1.module';
pageStartup(Page1Module, Page1Component);
```
## set Component pattern
- Set output Component pattern in `angular.json`, same with `assets` field
```json
"components":[
{
"glob": "**/*.entry.ts",
"input": "./src/components",
"output": "components"
}
]
```
## add a Component
- Create code with the following structure
```tree
├── component1.component.html
├── component1.component.scss
├── component1.component.ts
├── component1.entry.json
├── component1.entry.ts
└── component1.module.ts
```
- Create a `component1.entry.ts` file, add code as follow
```ts
import { componentRegistry } from 'angular-miniprogram';
import { Component1Component } from './component1.component';
componentRegistry(Component1Component);
```
- This code is registry Component as a miniprogram Component
- because of auto init, so use `Registry`
================================================
FILE: deploy/doc/index.md
================================================
---
layout: redirect
---
================================================
FILE: deploy/doc/zh-Hans/attention.md
================================================
---
layout: post
title: 注意事项
---
## 禁止使用
- 一切 dom 行为
- 带前缀的事件,如`bind`,`mut-bind`等,去掉冒号直接写,如事件`bind:tap`=>`bindtap`
> 前缀在 Angular 被解析为 target(window/document/body)
## 引入变更
- 使用`angular-miniprogram/common`代替`@angular/common`,
- 使用`angular-miniprogram/common/http`代替`@angular/common/http`
- 使用`angular-miniprogram/forms`代替`@angular/forms`
- 使用`import { HttpClientModule, provideHttpClient } from 'angular-miniprogram'`代替`import { HttpClientModule, provideHttpClient } from '@angular/common/http'`
## 注意
- 小程序的原生组件需要在`NgModule`中设置 `schemas:[NO_ERRORS_SCHEMA]` ,规避检测
- 元素属性赋值操作如果无法响应变更检测,使用`detectChanges`即可
- 目前,一个文件内只能有一个组件存在
## 兼容性支持
### 模板使用
- `createEmbeddedView`方法只能在结构型指令,或非结构型指令但是为模板引用`TemplateRef`中使用
- 当使用`createEmbeddedView`进行插入时,需要在上下文中的对象传递`__templateName`属性,这个属性为小程序的实际对应模板名
- 此模板名也可以访问`TemplateRef`实例的私有变量获得`(this.templateRef as any)._declarationTContainer.localNames[0]`
```ts
@Directive({
selector: '[appStructural1]',
})
export class Structural1Directive {
@Input() appStructural1: TemplateRef;
@Input() appStructural1Name: string;
constructor(private viewContainerRef: ViewContainerRef) {}
ngOnInit(): void {
this.viewContainerRef.createEmbeddedView(this.appStructural1, {
__templateName: this.appStructural1Name,
});
}
}
```
### 模板重命名
- 模板会在编译时编译为静态模板,所以可能存在重命名的情况,`如果`存在这种情况,可以使用多模板引用变量的方式实现
- ` ` `#name2`为可能重复的那个命名,`#name1`为编译到模板中的名字
- 第一个命名始终为模板的真实命名,但是所有的`模板引用变量`都可以引用
### 跨组件调用模板
- 在同一项目下,让其他组件接收到模板时,模板名定义需为`$$mp$$__self$$xxx`,这样就可以在同一项目下传递
> `同一application`或`同一library`下,都叫同一项目
```html
content
```
- 当需要给 library 组件中传入模板时,需要将模板名定义为`$$mp$$TemplateScopeName$$xxx`, `TemplateScopeName`生成规则如下
```ts
import { strings } from '@angular-devkit/core';
//library 为当前libary的名字
export function libraryTemplateScopeName(library: string) {
return strings.classify(library.replace(/[@/]/g, ''));
}
```
- 如`test-library`库为`TestLibrary`,`@my/library`库为`MyLibrary`
```html
```
### 未实现
- 控制流(control flow)
> 这部分功能目前看起来是动态生成的,要转换为静态的就只能用小程序原生的一些方法,但是又没法1:1对应,所以暂时没想到好方法
================================================
FILE: deploy/doc/zh-Hans/index.md
================================================
---
title: 'angular-miniprogram'
layout: home
---
使用 Angular 开发小程序
尽可能使用 Angular 已有生态,降低跨平台时所需成本
## 文档
- [快速启动](quick-start)
- [注意事项](attention)
- [小程序特性](miniprogram-feature)
- [API](../api-doc)
- [生命周期](life-time)
## 开发环境
### 前提条件
- `Node.js` 版本大于 20
- `@angular/cli` 版本为 17
### hello-world
- 使用[https://github.com/wszgrcy/angular-miniprogram-template](https://github.com/wszgrcy/angular-miniprogram-template)模板或将此项目下载
- 使用 npm 安装依赖
### 快速启动
- [快速启动](quick-start)
## 生态
- 支持`Angular`的全部服务及管道
- 兼容性支持指令(与 Angular 不同)
- 组件目前在一个文件中只能存在一个
## 支持平台
| 平台名 | 是否实现 | 关联 | 注释 |
| --------- | -------- | ------------------ | ----------------------------------------------------------------------------------------------------------------- |
| 微信 | ✅ | 企业微信 | |
| 字节跳动 | ✅ | | |
| 京东 | ❌ | | 没有账号 |
| 百度智能 | ✅ | | |
| 支付宝 | ✅ | 钉钉,支付宝 IoT 等 | 不能使用 Prod,问题[参见](https://forum.alipay.com/mini-app/post/65101060);不能使用基础库 2.0 编译,slot 有部分问题 |
| qq 小程序 | ✅ | | (非微信变种),但是事件有未知报错?但是好像也不影响 |
| 飞书 | ❌ | | 与字节跳动类似,但是编译有点问题 |
================================================
FILE: deploy/doc/zh-Hans/life-time.md
================================================
---
layout: post
title: 生命周期
---
# 组件(Component)
- created
> 等待链接(小程序 Component 与 ng Component 关联)
- properties 变更(第一次)
> 链接(ng自动初始化)
- attached
- ready
# 页面(Page)
- onLoad
> 初始化并链接
- onShow(第一次)
- onReady
# 页面(Component)
- onLoad
- onShow(第一次)
- created
> 初始化
- attached
> 链接
- onReady
# 组件(Component)在页面(Page)中
- ng-constructor(onLoad 开始)
> 页面组件
> 初始化并链接
- ng-constructor(component)
> 子组件
- ng-ngOnInit
- ng-ngAfterContentInit
- ng-ngOnInit(component)
- ng-ngAfterContentInit(component)
- ng-ngAfterViewInit(component)
- ng-ngAfterViewInit
- mp-created(component)
> 子组件链接(created之后,attached之前)
- mp-attached(component)
- mp-onLoad(onLoad 末尾)
- mp-onShow
- mp-onReady
- mp-ready(component)
================================================
FILE: deploy/doc/zh-Hans/miniprogram-feature.md
================================================
---
layout: post
title: 小程序特性
---
## 服务
- 通过`ComponentFinderService`服务来查询当前 ng 组件实例对应的小程序组件
```ts
import { ComponentFinderService } from 'angular-miniprogram';
//...
{
constructor(private componentFinderService: ComponentFinderService) {}
}
```
## token
- 通过`APP_TOKEN`可以获得 App 实例
- 通过`PAGE_TOKEN`可以获得组件对应的小程序页面实例
```ts
import {
APP_TOKEN,
PAGE_TOKEN,
} from 'angular-miniprogram';
//...
{
constructor(
@Inject(APP_TOKEN) appInstance: any,
@Inject(PAGE_TOKEN) pageInstance: any
) {}
}
```
## api
- `pageStartup`函数作为 page 的启动入口
```ts
import { pageStartup } from 'angular-miniprogram';
//...
pageStartup(Page1Module, Page1Component);
```
- `componentRegistry`组件注册
```ts
import { componentRegistry } from 'angular-miniprogram';
//...
componentRegistry(Component1Component);
```
## 组件上静态属性
- 在`Angular`@Component 组件中添加`static mpPageOptions`进行配置传参,与`Page({})`等同
> 当使用`pageStartup`函数时,使用此方法
```ts
@Component({
selector: 'app-life-time',
templateUrl: './life-time.component.html',
})
export class LifeTimePage implements OnInit {
static mpPageOptions: WechatMiniprogram.Page.Options<{}, {}> = {
onLoad: function (this: MiniProgramComponentInstance) {
console.log('mp-onLoad', this.__ngComponentInstance);
},
onShow: function () {
console.log('mp-onShow', this.__ngComponentInstance);
},
onReady: function (
this: WechatMiniprogram.Page.Instance<{}, {}> &
MiniProgramComponentInstance
) {
console.log('mp-onReady');
},
};
//...
}
```
- 在`Angular`@Component 组件中添加`static mpComponentOptions`进行配置传参,与`Component({})`等同
> 当使用`componentRegistry`函数时或使用`pageStartup`函数,但是 options 为`{useComponent:true}`,使用此方法
```ts
@Component({
selector: 'app-life-time-component',
templateUrl: './life-time.component.html',
standalone: true,
})
export class LifeTimeComponent implements OnInit {
static mpComponentOptions: WechatMiniprogram.Component.Options<{}, {}, {}> = {
lifetimes: {
created: function (this: MiniProgramComponentInstance) {
console.log('created(component)');
},
attached: function () {
console.log('attached(component)');
},
ready: function () {
console.log('ready(component)');
},
moved: function () {
console.log('moved(component)');
},
detached: function () {
console.log('detached(component)');
},
error: function () {
console.log('error(component)');
},
},
pageLifetimes: {
show: function () {
console.log('page-show(component)');
},
hide: function () {
console.log('page-hide(component)');
},
resize: function () {
console.log('page-resize(component)');
},
},
};
//...
}
```
## 全局变量
- `miniProgramPlatform`为一个 string 类型的全局变量,指示当前小程序的运行平台(`wx`,`zfb`,`zj`,`bdzn`,`qq`,`dd`等)
================================================
FILE: deploy/doc/zh-Hans/quick-start.md
================================================
---
layout: post
title: 快速启动
---
## 设置页面匹配
- 在`angular.json`中设置要匹配的页面范围,匹配规则与 assets 相同
```json
"pages": [
{
"glob": "**/*.entry.ts",
"input": "./src/pages",
"output": "pages"
}
]
```
- 上述配置为,在 src/pages 文件夹中,匹配`*.entry.ts`文件,并且编译完成导出在`[输出文件夹]/pages`文件夹中
## 添加一个页面
- 建立如下结构的代码
```tree
├── page1.component.html
├── page1.component.scss
├── page1.component.ts
├── page1.entry.json
├── page1.entry.ts
└── page1.module.ts
```
- 新建一个`page1.entry.ts`,输入如下
```ts
import { pageStartup } from 'angular-miniprogram';
import { Page1Component } from './page1.component';
import { Page1Module } from './page1.module';
pageStartup(Page1Module, Page1Component);
```
## 设置组件匹配
- 在`angular.json`中设置要匹配的组件范围,匹配规则与 assets 相同
```json
"components":[
{
"glob": "**/*.entry.ts",
"input": "./src/components",
"output": "components"
}
]
```
## 添加一个组件
- 建立如下结构的代码
```tree
├── component1.component.html
├── component1.component.scss
├── component1.component.ts
├── component1.entry.json
├── component1.entry.ts
└── component1.module.ts
```
- 新建一个`component1.entry.ts`,输入如下
```ts
import { componentRegistry } from 'angular-miniprogram';
import { Component1Component } from './component1.component';
componentRegistry(Component1Component);
```
- 上述代码为注册为将一个普通组件注册为小程序组件
- 由于该组件的使用为自动初始化,所以使用了`注册`
================================================
FILE: jasmine.json
================================================
{
"spec_dir": "src",
"spec_files": ["**/*.spec.ts"],
"failSpecWithNoExpectations": true,
"stopSpecOnExpectationFailure": true,
"stopOnSpecFailure": true,
"random": false
}
================================================
FILE: package.json
================================================
{
"name": "angular-miniprogram",
"version": "0.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test:ci": "npm run build:library && npm run test:jasmine library&& npm run test:jasmine",
"test": "npm run test:jasmine",
"test:jasmine": "tsc -p ./script/tsconfig.startup-jasmine.json && node ./script/startup-jasmine.js",
"prebuild": "rimraf ./dist",
"build": " npm run build:library && npm run build:builder && npm run copy:assets && npm run build:karma",
"copy:assets": "cpx \"./src/builder/**/*.json\" ./dist/builder && cpx \"./src/builder/**/*.js\" ./dist/builder && cpx ./readme.md ./dist",
"build:library": "node ./script/start-build-library",
"build:builder": "tsx ./script/build.ts ./tsconfig.builder.json",
"build:karma": "tsx ./script/build.ts ./src/builder/karma/client/tsconfig.json && tsx ./script/build.ts ./src/builder/karma/plugin/tsconfig.json",
"prepare": "husky install",
"lint": "eslint --max-warnings 0 \"./**/*.ts\"",
"sync": "code-recycle ./script/package-sync.ts --cwd ./src/library",
"coverage": "nyc npm run test",
"typedoc": "typedoc",
"deploy": "npm run typedoc && npm run build:library && npm run test:jasmine library && npm run coverage-badge -- init && cpx \"./deploy/doc/**/*\" ./docs && cpx \"./deploy/api-doc/**/*\" ./docs/api-doc && npm run coverage && npm run coverage-badge -- success && cpx ./deploy/doc/assets/img/badge.svg ./docs/assets/img",
"coverage-badge": "tsx ./script/coverage-badge.ts"
},
"private": true,
"author": "wszgrcy",
"license": "MIT",
"devDependencies": {
"@angular-devkit/architect": "0.1703.1",
"@angular-devkit/build-angular": "17.3.1",
"@angular-devkit/core": "17.3.1",
"@angular-devkit/schematics": "17.3.1",
"@angular/compiler": "17.3.1",
"@angular/compiler-cli": "17.3.1",
"@angular/core": "17.3.1",
"@code-recycle/cli": "^1.3.10",
"@commitlint/cli": "^13.1.0",
"@commitlint/config-conventional": "^13.1.0",
"@ngtools/webpack": "17.3.1",
"@types/fs-extra": "11.0.4",
"@types/glob": "^7.2.0",
"@types/jasmine": "^3.7.7",
"@types/karma": "^6.3.3",
"@types/node": "20.11.30",
"@types/socket.io-client": "^3.0.0",
"@typescript-eslint/eslint-plugin": "^5.44.0",
"@typescript-eslint/parser": "^5.44.0",
"cpx": "^1.5.0",
"cross-env": "^7.0.3",
"cyia-ngx-devkit": "0.0.5",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-import": "^2.26.0",
"fs-extra": "11.2.0",
"glob": "^7.2.0",
"husky": "^7.0.0",
"jasmine": "5.1.0",
"jasmine-core": "5.1.2",
"karma": "^6.3.17",
"mini-types": "^0.1.7",
"miniprogram-api-typings": "^3.12.2",
"ng-packagr": "17.3.0",
"prettier": "3.2.5",
"pretty-quick": "4.0.0",
"queue-microtask": "^1.2.3",
"rimraf": "5.0.5",
"rxjs": "^7.8.1",
"static-injector": "4.0.2",
"ts-node": "^10.9.2",
"tslib": "2.6.2",
"tsx": "^4.7.1",
"typedoc": "^0.25.12",
"typescript": "5.4.2",
"weapp.socket.io": "^3.0.0",
"webpack": "5.90.3",
"zone.js": "0.14.4"
},
"dependencies": {
"cyia-code-util": "^1.8.0",
"nyc": "^15.1.0",
"webpack-bootstrap-assets-plugin": "^2.0.3"
},
"resolutions": {}
}
================================================
FILE: readme.md
================================================
angular-miniprogram - 使用 Angular 开发小程序
尽可能使用 Angular 已有生态,降低跨平台时所需成本
- 文档 [https://wszgrcy.github.io/angular-miniprogram/](https://wszgrcy.github.io/angular-miniprogram/)
- document [https://wszgrcy.github.io/angular-miniprogram/en-US/](https://wszgrcy.github.io/angular-miniprogram/en-US/)
================================================
FILE: script/build-ng-package.ts
================================================
import * as path from 'path';
import { ngPackagrFactory } from '../src/builder/library/ng-packagr-factory';
async function main() {
let packagr = await ngPackagrFactory(
path.resolve(process.cwd(), './src/library/ng-package.json'),
path.resolve(process.cwd(), './tsconfig.library.json')
);
await packagr.build();
}
main();
================================================
FILE: script/build.ts
================================================
import * as path from 'path';
import * as fs from 'fs';
import * as ts from 'typescript';
import { createTransformer } from 'static-injector/transform';
export function main() {
let inputPath = process.argv[2];
let filePath = path.resolve(process.cwd(), inputPath);
let tsConfigBuffer = fs.readFileSync(filePath);
let jsonSourceFile = ts.parseJsonText(filePath, tsConfigBuffer.toString());
let config = ts.parseJsonSourceFileConfigFileContent(
jsonSourceFile,
ts.sys,
path.dirname(filePath)
);
let program = ts.createProgram({
rootNames: config.fileNames,
options: config.options,
projectReferences: config.projectReferences,
});
let errors = [
...program.getOptionsDiagnostics(),
...program.getGlobalDiagnostics(),
];
program.getSourceFiles().forEach((sf) => {
errors.push(
...program.getSyntacticDiagnostics(sf),
...program.getSemanticDiagnostics(sf)
);
});
if (errors.length) {
console.log(
ts.formatDiagnostics(errors, {
getCurrentDirectory: () => ts.sys.getCurrentDirectory(),
getNewLine: () => ts.sys.newLine,
getCanonicalFileName: (f: string) => f,
})
);
return;
}
let transformer = createTransformer(program);
program.emit(undefined, undefined, undefined, undefined, {
before: [transformer],
});
}
if (require.main === module) {
main();
}
================================================
FILE: script/coverage-badge.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
enum Status {
success = 'success',
init = 'init',
}
function main() {
let status = process.argv[2];
let outputPath = path.join(
process.cwd(),
'deploy',
'doc',
'assets',
'img',
'badge.svg'
);
let content: string;
if (status === Status.init) {
content = svgFailedGenerate();
} else if (status === Status.success) {
let file = fs
.readFileSync(
path.join(process.cwd(), 'docs', 'coverage', 'coverage-summary.json')
)
.toString();
let json = JSON.parse(file);
console.log(json.total.lines.pct);
content = svgGenerate(json.total.lines.pct + '%');
}
if (!fs.existsSync(path.dirname(outputPath))) {
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
}
fs.writeFileSync(outputPath, content);
}
function svgGenerate(percent: any) {
return `
CI
CI
${percent}
${percent}
`;
}
function svgFailedGenerate() {
return `
CI
CI
failing
failing
`;
}
main();
================================================
FILE: script/package-sync.ts
================================================
import {
type NodeQueryOption,
type ScriptFunction,
type FileQueryLayer,
completePromise,
fileBufferToString,
stringToFileBuffer,
} from '@code-recycle/cli';
const templateName = `__templateName`;
function getTemplateNameExpressionStr(templateRefName: string) {
return `(${templateRefName} as any)._declarationTContainer.localNames?(${templateRefName} as any)._declarationTContainer.localNames[0]:null`;
}
let fn: ScriptFunction = async (util, rule, host, injector) => {
let path = util.path;
let data = await rule.os.gitClone(
'https://github.com/angular/angular.git',
[
'/packages/common',
'/packages/forms',
'!/packages/common/test',
'!/packages/forms/test',
'!**/*.bazel',
'!**/*spec.ts',
'!**/*.js',
'!**/*.md',
],
'packages',
'branch',
'17.3.1'
);
let exclude = [
'forms/src/directives/default_value_accessor.ts',
'forms/src/directives/checkbox_value_accessor.ts',
// 'forms/src/directives/number_value_accessor.ts',
'forms/src/directives/radio_control_value_accessor.ts',
// 'forms/src/directives/range_value_accessor.ts',
// 'forms/src/directives/select_control_value_accessor.ts',
// 'forms/src/directives/select_multiple_control_value_accessor.ts',
'forms/src/directives.ts',
'forms/src/forms.ts',
];
for (const key in data) {
if (exclude.includes(key)) {
continue;
}
let buffer = data[key];
if (key.startsWith('common')) {
let content = fileBufferToString(buffer).replace(
/@angular\/common/g,
`angular-miniprogram/common`
);
buffer = stringToFileBuffer(content);
}
await completePromise(host.write(path.normalize(key), buffer));
}
let list = await util.changeList([
{
path: './common/src/directives/ng_for_of.ts',
list: [
{
query: `Constructor>CloseParenToken`,
insertBefore: true,
replace: `public ${templateName}:string`,
},
{
query: `NewExpression:like(new NgForOfContext)>CloseParenToken`,
insertBefore: true,
replace: `,${getTemplateNameExpressionStr('this._template')}`,
},
],
},
{
path: './common/src/directives/ng_if.ts',
list: [
{
query: `IfStatement:has(>PrefixUnaryExpression:like(this._thenViewRef) ) CallExpression:like(this._viewContainer.createEmbeddedView)>OpenParenToken+*::children(2)`,
replace: `{...{{''|ctxValue}},${templateName}:${getTemplateNameExpressionStr(
'this._thenTemplateRef'
)}}`,
},
{
query: `IfStatement:has(>PrefixUnaryExpression:like(this._elseViewRef) ) CallExpression:like(this._viewContainer.createEmbeddedView)>OpenParenToken+*::children(2)`,
replace: `{...{{''|ctxValue}},${templateName}:${getTemplateNameExpressionStr(
'this._elseTemplateRef'
)}}`,
},
{
query: `ClassDeclaration:has(>Identifier[value=NgIfContext])>CloseBraceToken`,
insertBefore: true,
replace: `public ${templateName}!:string`,
},
],
},
{
path: `./common/src/directives/ng_switch.ts`,
list: [
{
query: `CallExpression:like(this._viewContainerRef.createEmbeddedView)>CloseParenToken`,
insertBefore: true,
replace: `,{${templateName}:${getTemplateNameExpressionStr(
'this._templateRef'
)}}`,
},
],
},
{
path: `./common/src/directives/ng_template_outlet.ts`,
list: [
{
query: `CallExpression:like(viewContainerRef.createEmbeddedView)>OpenParenToken+*::children(2)`,
replace: `{...{{''|ctxValue}},${templateName}:${getTemplateNameExpressionStr(
'this.ngTemplateOutlet'
)}}as any`,
},
],
},
{
path: `./common/src/common.ts`,
list: [
{
query: `ExportDeclaration:has(StringLiteral[value*=i18n])`,
delete: true,
multi: true,
},
{
query: `ExportSpecifier[value^=I18n]:use(*,*+*)`,
delete: true,
multi: true,
},
{
query: `ExportSpecifier[value=NgComponentOutlet]:use(*,*+*)`,
delete: true,
multi: true,
},
],
},
{
path: `./common/src/pipes/index.ts`,
list: [
{
query: `ImportDeclaration:has(Identifier[value^=I18n])`,
multi: true,
delete: true,
},
{
query: `ExportSpecifier[value^=I18n]:use(*,*+*)`,
delete: true,
multi: true,
},
{
query: `ArrayLiteralExpression Identifier[value^=I18n]:use(*,*+*)`,
delete: true,
multi: true,
},
],
},
{
path: `./common/src/directives/index.ts`,
list: [
{
query: `ImportDeclaration:has(Identifier[value=NgComponentOutlet])`,
multi: true,
delete: true,
},
{
query: `ExportSpecifier[value=NgComponentOutlet]:use(*,*+*)`,
delete: true,
multi: true,
},
{
query: `ArrayLiteralExpression Identifier[value=NgComponentOutlet]:use(*,*+*)`,
delete: true,
multi: true,
},
],
},
]);
await util.updateChangeList(list);
};
export default fn;
================================================
FILE: script/registry-transformer.js
================================================
let { register } = require('ts-node');
let path = require('path');
let { createTransformer } = require('static-injector/transform');
module.exports = function registerTsNode() {
register({
project: path.resolve(__dirname, '../tsconfig.spec.json'),
transformers: (program) => {
const transformer = createTransformer(program);
return {
before: [transformer],
};
},
});
};
================================================
FILE: script/schema-merge.ts
================================================
import * as fs from 'fs';
import * as path from 'path';
function merge(origin: string, additional: string, output: string) {
origin = require.resolve(origin);
additional = require.resolve(additional);
let originFile = JSON.parse(fs.readFileSync(origin).toString());
let additionalFile = JSON.parse(fs.readFileSync(additional).toString());
let outputAbsolutePath = path.resolve(path.dirname(additional), output);
originFile['properties'] = {
...additionalFile['properties'],
...originFile['properties'],
};
originFile['definitions'] = {
...originFile['definitions'],
...additionalFile['definitions'],
};
fs.writeFileSync(
outputAbsolutePath,
JSON.stringify(originFile, undefined, 2)
);
}
function main() {
merge(
'@angular-devkit/build-angular/src/browser/schema.json',
'../src/builder/application/schema.base.json',
'./schema.json'
);
}
main();
================================================
FILE: script/start-build-library.js
================================================
let registerTsNode = require('./registry-transformer');
registerTsNode();
require('./build-ng-package');
================================================
FILE: script/startup-jasmine.ts
================================================
import { register } from 'ts-node';
import { createTransformer } from 'static-injector/transform';
import Jasmine from 'jasmine';
import path from 'path';
register({
project: path.resolve(__dirname, '../tsconfig.spec.json'),
transformers: (program) => {
const transformer = createTransformer(program);
return {
before: [transformer],
};
},
logError: true,
});
let jasmineInstance = new Jasmine();
let args = process.argv.slice(2) || [];
jasmineInstance.loadConfigFile(path.resolve(__dirname, '../jasmine.json'));
jasmineInstance.execute(undefined, args[0] || undefined);
================================================
FILE: script/tsconfig.json
================================================
{
"extends": "../tsconfig.base.json",
"compilerOptions": {
"esModuleInterop": true,
"downlevelIteration": true
},
"include": ["**/*.ts"],
"exclude": []
}
================================================
FILE: script/tsconfig.startup-jasmine.json
================================================
{
"extends": "./tsconfig.json",
"compilerOptions": { "skipLibCheck": true, "outDir": "." },
"files": ["./startup-jasmine.ts"],
"include": [],
"exclude": []
}
================================================
FILE: src/builder/angular-internal/ast.type.ts
================================================
import type {
TmplAstBoundAttribute,
TmplAstBoundEvent,
TmplAstBoundText,
TmplAstContent,
TmplAstElement,
TmplAstIcu,
TmplAstNode,
TmplAstRecursiveVisitor,
TmplAstReference,
TmplAstTemplate,
TmplAstText,
TmplAstTextAttribute,
TmplAstVariable,
} from '@angular/compiler';
export type Element = TmplAstElement;
export type Template = TmplAstTemplate;
export type Content = TmplAstContent;
export type Variable = TmplAstVariable;
export type Reference = TmplAstReference;
export type TextAttribute = TmplAstTextAttribute;
export type BoundAttribute = Parameters<
TmplAstRecursiveVisitor['visitBoundAttribute']
>[0];
export type BoundEvent = TmplAstBoundEvent;
export type Text = TmplAstText;
export type BoundText = TmplAstBoundText;
export type Icu = TmplAstIcu;
export type Node = TmplAstNode;
================================================
FILE: src/builder/angular-internal/selector.ts
================================================
/* eslint-disable @typescript-eslint/no-this-alias */
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-useless-escape */
/* eslint-disable no-irregular-whitespace */
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
const _SELECTOR_REGEXP = new RegExp(
'(\\:not\\()|' + // 1: ":not("
'(([\\.\\#]?)[-\\w]+)|' + // 2: "tag"; 3: "."/"#";
// "-" should appear first in the regexp below as FF31 parses "[.-\w]" as a range
// 4: attribute; 5: attribute_string; 6: attribute_value
'(?:\\[([-.\\w*\\\\$]+)(?:=(["\']?)([^\\]"\']*)\\5)?\\])|' + // "[name]", "[name=value]",
// "[name="value"]",
// "[name='value']"
'(\\))|' + // 7: ")"
'(\\s*,\\s*)', // 8: ","
'g'
);
/**
* These offsets should match the match-groups in `_SELECTOR_REGEXP` offsets.
*/
const enum SelectorRegexp {
ALL = 0, // The whole match
NOT = 1,
TAG = 2,
PREFIX = 3,
ATTRIBUTE = 4,
ATTRIBUTE_STRING = 5,
ATTRIBUTE_VALUE = 6,
NOT_END = 7,
SEPARATOR = 8,
}
/**
* A css selector contains an element name,
* css classes and attribute/value pairs with the purpose
* of selecting subsets out of them.
*/
export class CssSelector {
element: string | null = null;
classNames: string[] = [];
/**
* The selectors are encoded in pairs where:
* - even locations are attribute names
* - odd locations are attribute values.
*
* Example:
* Selector: `[key1=value1][key2]` would parse to:
* ```
* ['key1', 'value1', 'key2', '']
* ```
*/
attrs: string[] = [];
notSelectors: CssSelector[] = [];
static parse(selector: string): CssSelector[] {
const results: CssSelector[] = [];
const _addResult = (res: CssSelector[], cssSel: CssSelector) => {
if (
cssSel.notSelectors.length > 0 &&
!cssSel.element &&
cssSel.classNames.length == 0 &&
cssSel.attrs.length == 0
) {
cssSel.element = '*';
}
res.push(cssSel);
};
let cssSelector = new CssSelector();
let match: string[] | null;
let current = cssSelector;
let inNot = false;
_SELECTOR_REGEXP.lastIndex = 0;
// eslint-disable-next-line no-cond-assign
while ((match = _SELECTOR_REGEXP.exec(selector))) {
if (match[SelectorRegexp.NOT]) {
if (inNot) {
throw new Error('Nesting :not in a selector is not allowed');
}
inNot = true;
current = new CssSelector();
cssSelector.notSelectors.push(current);
}
const tag = match[SelectorRegexp.TAG];
if (tag) {
const prefix = match[SelectorRegexp.PREFIX];
if (prefix === '#') {
// #hash
current.addAttribute('id', tag.substr(1));
} else if (prefix === '.') {
// Class
current.addClassName(tag.substr(1));
} else {
// Element
current.setElement(tag);
}
}
const attribute = match[SelectorRegexp.ATTRIBUTE];
if (attribute) {
current.addAttribute(
current.unescapeAttribute(attribute),
match[SelectorRegexp.ATTRIBUTE_VALUE]
);
}
if (match[SelectorRegexp.NOT_END]) {
inNot = false;
current = cssSelector;
}
if (match[SelectorRegexp.SEPARATOR]) {
if (inNot) {
throw new Error('Multiple selectors in :not are not supported');
}
_addResult(results, cssSelector);
cssSelector = current = new CssSelector();
}
}
_addResult(results, cssSelector);
return results;
}
/**
* Unescape `\$` sequences from the CSS attribute selector.
*
* This is needed because `$` can have a special meaning in CSS selectors,
* but we might want to match an attribute that contains `$`.
* [MDN web link for more
* info](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors).
* @param attr the attribute to unescape.
* @returns the unescaped string.
*/
unescapeAttribute(attr: string): string {
let result = '';
let escaping = false;
for (let i = 0; i < attr.length; i++) {
const char = attr.charAt(i);
if (char === '\\') {
escaping = true;
continue;
}
if (char === '$' && !escaping) {
throw new Error(
`Error in attribute selector "${attr}". ` +
`Unescaped "$" is not supported. Please escape with "\\$".`
);
}
escaping = false;
result += char;
}
return result;
}
/**
* Escape `$` sequences from the CSS attribute selector.
*
* This is needed because `$` can have a special meaning in CSS selectors,
* with this method we are escaping `$` with `\$'.
* [MDN web link for more
* info](https://developer.mozilla.org/en-US/docs/Web/CSS/Attribute_selectors).
* @param attr the attribute to escape.
* @returns the escaped string.
*/
escapeAttribute(attr: string): string {
return attr.replace(/\\/g, '\\\\').replace(/\$/g, '\\$');
}
hasElementSelector(): boolean {
return !!this.element;
}
setElement(element: string | null = null) {
this.element = element;
}
addAttribute(name: string, value: string = '') {
this.attrs.push(name, (value && value.toLowerCase()) || '');
}
addClassName(name: string) {
this.classNames.push(name.toLowerCase());
}
toString(): string {
let res: string = this.element || '';
if (this.classNames) {
this.classNames.forEach((klass) => (res += `.${klass}`));
}
if (this.attrs) {
for (let i = 0; i < this.attrs.length; i += 2) {
const name = this.escapeAttribute(this.attrs[i]);
const value = this.attrs[i + 1];
res += `[${name}${value ? '=' + value : ''}]`;
}
}
this.notSelectors.forEach((notSelector) => (res += `:not(${notSelector})`));
return res;
}
}
/**
* Reads a list of CssSelectors and allows to calculate which ones
* are contained in a given CssSelector.
*/
export class SelectorMatcher {
static createNotMatcher(notSelectors: CssSelector[]): SelectorMatcher {
const notMatcher = new SelectorMatcher();
notMatcher.addSelectables(notSelectors, null);
return notMatcher;
}
private _elementMap = new Map[]>();
private _elementPartialMap = new Map>();
private _classMap = new Map[]>();
private _classPartialMap = new Map>();
private _attrValueMap = new Map[]>>();
private _attrValuePartialMap = new Map<
string,
Map>
>();
private _listContexts: SelectorListContext[] = [];
addSelectables(cssSelectors: CssSelector[], callbackCtxt?: T) {
let listContext: SelectorListContext = null!;
if (cssSelectors.length > 1) {
listContext = new SelectorListContext(cssSelectors);
this._listContexts.push(listContext);
}
for (let i = 0; i < cssSelectors.length; i++) {
this._addSelectable(cssSelectors[i], callbackCtxt as T, listContext);
}
}
/**
* Add an object that can be found later on by calling `match`.
* @param cssSelector A css selector
* @param callbackCtxt An opaque object that will be given to the callback of the `match` function
*/
private _addSelectable(
cssSelector: CssSelector,
callbackCtxt: T,
listContext: SelectorListContext
) {
let matcher: SelectorMatcher = this;
const element = cssSelector.element;
const classNames = cssSelector.classNames;
const attrs = cssSelector.attrs;
const selectable = new SelectorContext(
cssSelector,
callbackCtxt,
listContext
);
if (element) {
const isTerminal = attrs.length === 0 && classNames.length === 0;
if (isTerminal) {
this._addTerminal(matcher._elementMap, element, selectable);
} else {
matcher = this._addPartial(matcher._elementPartialMap, element);
}
}
if (classNames) {
for (let i = 0; i < classNames.length; i++) {
const isTerminal = attrs.length === 0 && i === classNames.length - 1;
const className = classNames[i];
if (isTerminal) {
this._addTerminal(matcher._classMap, className, selectable);
} else {
matcher = this._addPartial(matcher._classPartialMap, className);
}
}
}
if (attrs) {
for (let i = 0; i < attrs.length; i += 2) {
const isTerminal = i === attrs.length - 2;
const name = attrs[i];
const value = attrs[i + 1];
if (isTerminal) {
const terminalMap = matcher._attrValueMap;
let terminalValuesMap = terminalMap.get(name);
if (!terminalValuesMap) {
terminalValuesMap = new Map[]>();
terminalMap.set(name, terminalValuesMap);
}
this._addTerminal(terminalValuesMap, value, selectable);
} else {
const partialMap = matcher._attrValuePartialMap;
let partialValuesMap = partialMap.get(name);
if (!partialValuesMap) {
partialValuesMap = new Map>();
partialMap.set(name, partialValuesMap);
}
matcher = this._addPartial(partialValuesMap, value);
}
}
}
}
private _addTerminal(
map: Map[]>,
name: string,
selectable: SelectorContext
) {
let terminalList = map.get(name);
if (!terminalList) {
terminalList = [];
map.set(name, terminalList);
}
terminalList.push(selectable);
}
private _addPartial(
map: Map>,
name: string
): SelectorMatcher {
let matcher = map.get(name);
if (!matcher) {
matcher = new SelectorMatcher();
map.set(name, matcher);
}
return matcher;
}
/**
* Find the objects that have been added via `addSelectable`
* whose css selector is contained in the given css selector.
* @param cssSelector A css selector
* @param matchedCallback This callback will be called with the object handed into `addSelectable`
* @return boolean true if a match was found
*/
match(
cssSelector: CssSelector,
matchedCallback: ((c: CssSelector, a: T) => void) | null
): boolean {
let result = false;
const element = cssSelector.element!;
const classNames = cssSelector.classNames;
const attrs = cssSelector.attrs;
for (let i = 0; i < this._listContexts.length; i++) {
this._listContexts[i].alreadyMatched = false;
}
result =
this._matchTerminal(
this._elementMap,
element,
cssSelector,
matchedCallback
) || result;
result =
this._matchPartial(
this._elementPartialMap,
element,
cssSelector,
matchedCallback
) || result;
if (classNames) {
for (let i = 0; i < classNames.length; i++) {
const className = classNames[i];
result =
this._matchTerminal(
this._classMap,
className,
cssSelector,
matchedCallback
) || result;
result =
this._matchPartial(
this._classPartialMap,
className,
cssSelector,
matchedCallback
) || result;
}
}
if (attrs) {
for (let i = 0; i < attrs.length; i += 2) {
const name = attrs[i];
const value = attrs[i + 1];
const terminalValuesMap = this._attrValueMap.get(name)!;
if (value) {
result =
this._matchTerminal(
terminalValuesMap,
'',
cssSelector,
matchedCallback
) || result;
}
result =
this._matchTerminal(
terminalValuesMap,
value,
cssSelector,
matchedCallback
) || result;
const partialValuesMap = this._attrValuePartialMap.get(name)!;
if (value) {
result =
this._matchPartial(
partialValuesMap,
'',
cssSelector,
matchedCallback
) || result;
}
result =
this._matchPartial(
partialValuesMap,
value,
cssSelector,
matchedCallback
) || result;
}
}
return result;
}
/** @internal */
_matchTerminal(
map: Map[]>,
name: string,
cssSelector: CssSelector,
matchedCallback: ((c: CssSelector, a: any) => void) | null
): boolean {
if (!map || typeof name !== 'string') {
return false;
}
let selectables: SelectorContext[] = map.get(name) || [];
const starSelectables: SelectorContext[] = map.get('*')!;
if (starSelectables) {
selectables = selectables.concat(starSelectables);
}
if (selectables.length === 0) {
return false;
}
let selectable: SelectorContext;
let result = false;
for (let i = 0; i < selectables.length; i++) {
selectable = selectables[i];
result = selectable.finalize(cssSelector, matchedCallback) || result;
}
return result;
}
/** @internal */
_matchPartial(
map: Map>,
name: string,
cssSelector: CssSelector,
matchedCallback: ((c: CssSelector, a: any) => void) | null
): boolean {
if (!map || typeof name !== 'string') {
return false;
}
const nestedSelector = map.get(name);
if (!nestedSelector) {
return false;
}
// TODO(perf): get rid of recursion and measure again
// TODO(perf): don't pass the whole selector into the recursion,
// but only the not processed parts
return nestedSelector.match(cssSelector, matchedCallback);
}
}
export class SelectorListContext {
alreadyMatched: boolean = false;
constructor(public selectors: CssSelector[]) {}
}
// Store context to pass back selector and context when a selector is matched
export class SelectorContext {
notSelectors: CssSelector[];
constructor(
public selector: CssSelector,
public cbContext: T,
public listContext: SelectorListContext
) {
this.notSelectors = selector.notSelectors;
}
finalize(
cssSelector: CssSelector,
callback: ((c: CssSelector, a: T) => void) | null
): boolean {
let result = true;
if (
this.notSelectors.length > 0 &&
(!this.listContext || !this.listContext.alreadyMatched)
) {
const notMatcher = SelectorMatcher.createNotMatcher(this.notSelectors);
result = !notMatcher.match(cssSelector, null);
}
if (
result &&
callback &&
(!this.listContext || !this.listContext.alreadyMatched)
) {
if (this.listContext) {
this.listContext.alreadyMatched = true;
}
callback(this.selector, this.cbContext);
}
return result;
}
}
================================================
FILE: src/builder/angular-internal/tags.ts
================================================
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
export enum TagContentType {
RAW_TEXT,
ESCAPABLE_RAW_TEXT,
PARSABLE_DATA,
}
export interface TagDefinition {
closedByParent: boolean;
implicitNamespacePrefix: string | null;
isVoid: boolean;
ignoreFirstLf: boolean;
canSelfClose: boolean;
preventNamespaceInheritance: boolean;
isClosedByChild(name: string): boolean;
getContentType(prefix?: string): TagContentType;
}
export function splitNsName(elementName: string): [string | null, string] {
if (elementName[0] != ':') {
return [null, elementName];
}
const colonIndex = elementName.indexOf(':', 1);
if (colonIndex === -1) {
throw new Error(
`Unsupported format "${elementName}" expecting ":namespace:name"`
);
}
return [elementName.slice(1, colonIndex), elementName.slice(colonIndex + 1)];
}
// `` tags work the same regardless the namespace
export function isNgContainer(tagName: string): boolean {
return splitNsName(tagName)[1] === 'ng-container';
}
// `` tags work the same regardless the namespace
export function isNgContent(tagName: string): boolean {
return splitNsName(tagName)[1] === 'ng-content';
}
// `` tags work the same regardless the namespace
export function isNgTemplate(tagName: string): boolean {
return splitNsName(tagName)[1] === 'ng-template';
}
export function getNsPrefix(fullName: string): string;
export function getNsPrefix(fullName: null): null;
export function getNsPrefix(fullName: string | null): string | null {
return fullName === null ? null : splitNsName(fullName)[0];
}
export function mergeNsAndName(prefix: string, localName: string): string {
return prefix ? `:${prefix}:${localName}` : localName;
}
================================================
FILE: src/builder/angular-internal/template.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import type { CssSelector as CssSelectorType } from '@angular/compiler';
import { CssSelector } from './selector';
import { splitNsName } from './tags';
/**
* Creates a `CssSelector` given a tag name and a map of attributes
*/
export function createCssSelector(
elementName: string,
attributes: { [name: string]: string }
): CssSelectorType {
const cssSelector = new CssSelector();
const elementNameNoNs = splitNsName(elementName)[1];
cssSelector.setElement(elementNameNoNs);
Object.getOwnPropertyNames(attributes).forEach((name) => {
const nameNoNs = splitNsName(name)[1];
const value = attributes[name];
cssSelector.addAttribute(nameNoNs, value);
if (name.toLowerCase() === 'class') {
const classes = value.trim().split(/\s+/);
classes.forEach((className) => cssSelector.addClassName(className));
}
});
return cssSelector as any;
}
================================================
FILE: src/builder/angular-internal/util.ts
================================================
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import type * as t from './ast.type';
/**
* Extract a map of properties to values for a given element or template node, which can be used
* by the directive matching machinery.
*
* @param elOrTpl the element or template in question
* @return an object set up for directive matching. For attributes on the element/template, this
* object maps a property name to its (static) value. For any bindings, this map simply maps the
* property name to an empty string.
*/
export function getAttrsForDirectiveMatching(elOrTpl: t.Element): {
[name: string]: string;
} {
const attributesMap: { [name: string]: string } = {};
elOrTpl.attributes.forEach((a: { name: string; value: string }) => {
if (!isI18nAttribute(a.name)) {
attributesMap[a.name] = a.value;
}
});
elOrTpl.inputs.forEach((i: { name: string | number }) => {
attributesMap[i.name] = '';
});
elOrTpl.outputs.forEach((o: { name: string | number }) => {
attributesMap[o.name] = '';
});
return attributesMap;
}
/** Name of the i18n attributes **/
export const I18N_ATTR = 'i18n';
export const I18N_ATTR_PREFIX = 'i18n-';
export function isI18nAttribute(name: string): boolean {
return name === I18N_ATTR || name.startsWith(I18N_ATTR_PREFIX);
}
================================================
FILE: src/builder/application/const.ts
================================================
export const ExportMiniProgramAssetsPluginSymbol = Symbol.for(
'ExportMiniProgramAssetsPluginSymbol'
);
export const LibrarySymbol = Symbol.for('LibrarySymbol');
export const InjectorSymbol = Symbol.for('InjectorSymbol');
export const TemplateScopeSymbol = Symbol.for('TemplateScopeSymbol');
================================================
FILE: src/builder/application/index.ts
================================================
import type { BuilderContext } from '@angular-devkit/architect';
import { createBuilder } from '@angular-devkit/architect';
import type {
AssetPattern,
BrowserBuilderOptions,
} from '@angular-devkit/build-angular';
import { executeBrowserBuilder } from '@angular-devkit/build-angular';
import { Injector } from 'static-injector';
import * as webpack from 'webpack';
import { PlatformType } from '../platform/platform';
import { getBuildPlatformInjectConfig } from '../platform/platform-inject-config';
import { WebpackConfigurationChangeService } from './webpack-configuration-change.service';
export default createBuilder(
(
angularOptions: BrowserBuilderOptions & {
pages: AssetPattern[];
components: AssetPattern[];
platform: PlatformType;
},
context: BuilderContext
): ReturnType => {
return runBuilder(angularOptions, context);
}
);
export function runBuilder(
angularOptions: BrowserBuilderOptions & {
pages: AssetPattern[];
components: AssetPattern[];
platform: PlatformType;
},
context: BuilderContext
): ReturnType {
return executeBrowserBuilder(angularOptions, context, {
webpackConfiguration: async (options: webpack.Configuration) => {
const injector = Injector.create({
providers: [
...getBuildPlatformInjectConfig(angularOptions.platform),
{
provide: WebpackConfigurationChangeService,
useFactory: (injector: Injector) => {
return new WebpackConfigurationChangeService(
angularOptions,
context,
options,
injector
);
},
deps: [Injector],
},
],
});
const config = injector.get(WebpackConfigurationChangeService);
config.init();
await config.change();
return options;
},
});
}
================================================
FILE: src/builder/application/library-template-scope.service.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Injectable } from 'static-injector';
import * as webpack from 'webpack';
import { TemplateScopeSymbol } from './const';
export type TemplateScopeOutside = Omit<
LibraryTemplateScopeService,
Exclude<
keyof LibraryTemplateScopeService,
'setScopeLibraryUseComponents' | 'setScopeExtraUseComponents'
>
>;
export interface ExtraTemplateData {
useComponents: Record;
templateList: string[];
configPath?: string;
templatePath?: string;
}
@Injectable()
export class LibraryTemplateScopeService {
private scopeExtraUseComponentsMap = new Map();
private scopeLibraryUseComponentsMap = new Map();
// 追加模板
constructor() {}
register(compilation: webpack.Compilation) {
(compilation as any)[TemplateScopeSymbol] = {
setScopeExtraUseComponents: this.setScopeExtraUseComponents,
setScopeLibraryUseComponents: this.setScopeLibraryUseComponents,
} as TemplateScopeOutside;
}
exportLibraryComponentConfig() {
const list: {
filePath: string;
content: { component: boolean; usingComponents: Record };
}[] = [];
this.scopeLibraryUseComponentsMap.forEach((obj, libraryScope) => {
const extraData = this.scopeExtraUseComponentsMap.get(libraryScope) || {
useComponents: {},
};
// if (!extraData) {
// throw new Error(`没有找到${libraryScope}对应的配置`);
// }
for (const item of obj) {
const configPath = item.configPath!;
const usingComponents = {
...item.useComponents,
...extraData.useComponents,
};
list.push({
filePath: configPath,
content: { component: true, usingComponents: usingComponents },
});
}
});
return list;
}
exportLibraryTemplate() {
const fileGroup: Record = {};
this.scopeLibraryUseComponentsMap.forEach((obj, libraryScope) => {
const extraData = this.scopeExtraUseComponentsMap.get(libraryScope) || {
templateList: [],
};
// if (!extraData) {
// throw new Error(`没有找到${libraryScope}对应的配置`);
// }
for (const item of obj) {
if (fileGroup[item.templatePath!]) {
continue;
}
fileGroup[item.templatePath!] = extraData.templateList.join('');
}
});
return fileGroup;
}
setScopeExtraUseComponents = (
libraryScope: string,
extraData: ExtraTemplateData
) => {
const data: ExtraTemplateData = this.scopeExtraUseComponentsMap.get(
libraryScope
) || { useComponents: {}, templateList: [] };
this.scopeExtraUseComponentsMap.set(libraryScope, {
useComponents: { ...data.useComponents, ...extraData.useComponents },
templateList: [...data.templateList, ...extraData.templateList],
});
};
setScopeLibraryUseComponents = (
libraryScope: string,
libraryUseComponents: ExtraTemplateData[]
) => {
this.scopeLibraryUseComponentsMap.set(libraryScope, libraryUseComponents);
};
}
================================================
FILE: src/builder/application/loader/component-template.loader.ts
================================================
import * as webpack from 'webpack';
import { changeComponent } from '../../component-template-inject/change-component';
export default async function (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this: webpack.LoaderContext,
data: string,
map: string
) {
const callback = this.async();
const changeData = changeComponent(data)!;
if (typeof data === 'undefined' || typeof changeData === 'undefined') {
callback(undefined, data, map);
return;
}
callback(undefined, changeData.content);
}
================================================
FILE: src/builder/application/loader/library-template.loader.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { createCssSelectorForTs } from 'cyia-code-util';
import { Injector } from 'static-injector';
import { VariableDeclaration } from 'typescript';
import * as webpack from 'webpack';
import { TemplateScopeOutside } from '../../application/library-template-scope.service';
import { GLOBAL_TEMPLATE_SUFFIX } from '../../library';
import { ExtraTemplateData } from '../../library/type';
import { BuildPlatform } from '../../platform/platform';
import { literalResolve } from '../../util';
import { InjectorSymbol, TemplateScopeSymbol } from '../const';
import { LibraryTemplateLiteralConvertOptions } from '../type';
export default async function (
this: webpack.LoaderContext,
data: string,
map: string
) {
const callback = this.async();
const selector = createCssSelectorForTs(data);
const injector: Injector = (this._compilation! as any)[InjectorSymbol];
const buildPlatform = injector.get(BuildPlatform);
const templateScopeOutside = (this._compilation as any)[
TemplateScopeSymbol
] as TemplateScopeOutside;
const selfTemplateNode = selector.queryOne(
`VariableDeclaration[name="$self_${GLOBAL_TEMPLATE_SUFFIX}"]`
) as VariableDeclaration;
if (selfTemplateNode) {
const content = selfTemplateNode.initializer!.getText();
const config: ExtraTemplateData = literalResolve(content);
this.emitFile(
config.outputPath + buildPlatform.fileExtname.contentTemplate,
literalResolve(
`\`${config.template}\``,
{
directivePrefix:
buildPlatform.templateTransform.getData().directivePrefix,
eventListConvert: buildPlatform.templateTransform.eventListConvert,
templateInterpolation:
buildPlatform.templateTransform.templateInterpolation,
fileExtname: buildPlatform.fileExtname,
}
)
);
}
const libraryTemplateNode = selector.queryOne(
`VariableDeclaration[name="library_${GLOBAL_TEMPLATE_SUFFIX}"]`
) as VariableDeclaration;
if (libraryTemplateNode) {
const content = libraryTemplateNode.initializer!.getText();
const config: Record = literalResolve(content);
for (const key in config) {
if (Object.prototype.hasOwnProperty.call(config, key)) {
const element = config[key];
templateScopeOutside.setScopeExtraUseComponents(key, {
useComponents: element.useComponents!,
templateList: [element.template],
});
}
}
}
callback(undefined, data, map);
}
================================================
FILE: src/builder/application/loader/library.loader.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { dirname, join, normalize, strings } from '@angular-devkit/core';
import { createCssSelectorForTs } from 'cyia-code-util';
import ts from 'typescript';
import * as webpack from 'webpack';
import {
ExtraTemplateData,
TemplateScopeOutside,
} from '../../application/library-template-scope.service';
import {
LIBRARY_COMPONENT_METADATA_SUFFIX,
LIBRARY_OUTPUT_ROOTDIR,
} from '../../library';
import type { ExportLibraryComponentMeta } from '../../library';
import { libraryTemplateScopeName, literalResolve } from '../../util';
import {
ExportMiniProgramAssetsPluginSymbol,
LibrarySymbol,
TemplateScopeSymbol,
} from '../const';
import type { LibraryLoaderContext } from '../type';
import { LibraryTemplateLiteralConvertOptions } from '../type';
import { ComponentTemplateLoaderContext } from './type';
export default async function (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this: webpack.LoaderContext,
data: string,
map: string
) {
const callback = this.async();
const selector = createCssSelectorForTs(data);
const list = selector.queryAll(
`PropertyAccessExpression[name=ɵɵdefineComponent]~SyntaxList ObjectLiteralExpression PropertyAssignment[name=type]::initializer`
);
if (!list.length) {
callback(undefined, data, map);
return;
}
const context: ComponentTemplateLoaderContext = (this._compilation! as any)[
ExportMiniProgramAssetsPluginSymbol
];
const templateScopeOutside = (this._compilation as any)[
TemplateScopeSymbol
] as TemplateScopeOutside;
const scopeLibraryObj: Record = {};
for (let i = 0; i < list.length; i++) {
const element = list[i] as ts.BinaryExpression;
const componentName = element.getText();
const extraNode = selector.queryOne(
`VariableDeclaration[name="${componentName}_${LIBRARY_COMPONENT_METADATA_SUFFIX}"]`
) as ts.VariableDeclaration;
if (!extraNode) {
continue;
}
const content = extraNode.initializer!.getText();
const meta: ExportLibraryComponentMeta = literalResolve(content);
(this._compilation as any)[LibrarySymbol] =
(this._compilation as any)[LibrarySymbol] || {};
const libraryLoaderContext: LibraryLoaderContext = (
this._compilation as any
)[LibrarySymbol];
libraryLoaderContext.libraryMetaList =
libraryLoaderContext.libraryMetaList || [];
libraryLoaderContext.libraryMetaList.push({
...meta,
context: this.context,
importPath: this.resourcePath,
contextPath: this.utils.contextify(this.rootContext, this.resourcePath),
});
const fileExtname = libraryLoaderContext.buildPlatform.fileExtname;
libraryLoaderContext.libraryMetaList.forEach((item) => {
const globalTemplatePath = join(
normalize('/library-template'),
strings.classify(item.moduleId) +
libraryLoaderContext.buildPlatform.fileExtname.contentTemplate
);
const LIBRARY_SCOPE_ID = libraryTemplateScopeName(item.moduleId);
const configPath = join(
normalize(LIBRARY_OUTPUT_ROOTDIR),
item.libraryPath + fileExtname.config
);
const list = scopeLibraryObj[LIBRARY_SCOPE_ID] || [];
list.push({
configPath: configPath,
useComponents: item.useComponents,
templateList: [],
templatePath: globalTemplatePath,
});
scopeLibraryObj[LIBRARY_SCOPE_ID] = list;
const libraryTemplateLiteralConvertOptions: LibraryTemplateLiteralConvertOptions =
{
directivePrefix:
libraryLoaderContext.buildPlatform.templateTransform.getData()
.directivePrefix,
eventListConvert:
libraryLoaderContext.buildPlatform.templateTransform
.eventListConvert,
templateInterpolation:
libraryLoaderContext.buildPlatform.templateTransform
.templateInterpolation,
fileExtname: libraryLoaderContext.buildPlatform.fileExtname,
};
this.emitFile(
join(
normalize(LIBRARY_OUTPUT_ROOTDIR),
item.libraryPath + fileExtname.content
),
` ` +
literalResolve(
`\`${item.content}\``,
libraryTemplateLiteralConvertOptions
)
);
if (item.contentTemplate) {
this.emitFile(
join(
normalize(LIBRARY_OUTPUT_ROOTDIR),
dirname(normalize(item.libraryPath)),
'template' + fileExtname.contentTemplate
),
` ` +
literalResolve(
`\`${item.contentTemplate}\``,
libraryTemplateLiteralConvertOptions
)
);
}
if (item.style) {
this.emitFile(
join(
normalize(LIBRARY_OUTPUT_ROOTDIR),
item.libraryPath + fileExtname.style
),
item.style
);
}
});
}
for (const key in scopeLibraryObj) {
if (Object.prototype.hasOwnProperty.call(scopeLibraryObj, key)) {
const element = scopeLibraryObj[key];
templateScopeOutside.setScopeLibraryUseComponents(key, element);
}
}
callback(undefined, data, map);
}
================================================
FILE: src/builder/application/loader/type.ts
================================================
import { MetaCollection } from '../../mini-program-compiler';
import type { BuildPlatform } from '../../platform/platform';
export interface ComponentTemplateLoaderContext {
buildPlatform: BuildPlatform;
otherMetaGroupPromise: Promise>;
}
================================================
FILE: src/builder/application/mini-program-application-analysis.service.ts
================================================
import type { NgtscProgram, ParsedConfiguration } from '@angular/compiler-cli';
import type { NgCompiler } from '@angular/compiler-cli/src/ngtsc/core';
import { join, normalize, resolve } from '@angular-devkit/core';
import { externalizePath } from '@ngtools/webpack/src/ivy/paths';
import { createHash } from 'crypto';
import { createCssSelectorForTs } from 'cyia-code-util';
import * as path from 'path';
import { Inject, Injectable, Injector } from 'static-injector';
import ts from 'typescript';
import type { CompilerOptions } from 'typescript';
import { Compilation, Compiler } from 'webpack';
import { LIBRARY_OUTPUT_ROOTDIR } from '../library';
import { MiniProgramCompilerService } from '../mini-program-compiler';
import { BuildPlatform } from '../platform/platform';
import { angularCompilerCliPromise } from '../util/load_esm';
import {
OLD_BUILDER,
PAGE_PATTERN_TOKEN,
TS_CONFIG_TOKEN,
TS_SYSTEM,
WEBPACK_COMPILATION,
WEBPACK_COMPILER,
} from './token';
import type { PagePattern } from './type';
@Injectable()
export class MiniProgramApplicationAnalysisService {
private dependencyUseModule = new Map();
private cleanDependencyFileCacheSet = new Set();
builder!: ts.BuilderProgram | ts.EmitAndSemanticDiagnosticsBuilderProgram;
private ngTscProgram!: NgtscProgram;
private tsProgram!: ts.Program;
private ngCompiler!: NgCompiler;
private typeChecker!: ts.TypeChecker;
constructor(
private injector: Injector,
@Inject(WEBPACK_COMPILATION) private compilation: Compilation,
@Inject(TS_SYSTEM) private system: ts.System,
@Inject(WEBPACK_COMPILER) private compiler: Compiler,
@Inject(TS_CONFIG_TOKEN) private tsConfig: string,
@Inject(OLD_BUILDER)
private oldBuilder: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined,
@Inject(PAGE_PATTERN_TOKEN) private pagePatternList: PagePattern[],
private buildPlatform: BuildPlatform
) {}
async exportComponentBuildMetaMap() {
const injector = Injector.create({
providers: [
{
provide: MiniProgramCompilerService,
useFactory: (injector: Injector, buildPlatform: BuildPlatform) => {
return new MiniProgramCompilerService(
this.ngTscProgram,
injector,
buildPlatform
);
},
deps: [Injector, BuildPlatform],
},
],
parent: this.injector,
});
const miniProgramCompilerService = injector.get(MiniProgramCompilerService);
miniProgramCompilerService.init();
const metaMap =
await miniProgramCompilerService.exportComponentBuildMetaMap();
const selfMetaCollection = metaMap.otherMetaCollectionGroup['$self'];
const selfTemplate: Record = {};
if (selfMetaCollection) {
const importSelfTemplatePath = `/self-template/self${this.buildPlatform.fileExtname.contentTemplate}`;
const importSelfTemplate = ` `;
metaMap.outputContent.forEach((value, key) => {
value = `${importSelfTemplate}${value}`;
metaMap.outputContent.set(key, value);
});
metaMap.useComponentPath.forEach((value, key) => {
value.libraryPath.push(...selfMetaCollection.libraryPath);
value.localPath.push(...selfMetaCollection.localPath);
});
selfTemplate[importSelfTemplatePath] = selfMetaCollection.templateList
.map((item) => item.content)
.join('');
delete metaMap.otherMetaCollectionGroup['$self'];
}
metaMap.useComponentPath.forEach((value, key) => {
value.libraryPath = Array.from(new Set(value.libraryPath));
value.localPath = Array.from(new Set(value.localPath));
});
const styleMap = new Map();
metaMap.style.forEach((value, key) => {
const entryPattern = this.getComponentPagePattern(key);
styleMap.set(entryPattern.outputFiles.style, value);
});
const contentMap = new Map();
metaMap.outputContent.forEach((value, key) => {
const entryPattern = this.getComponentPagePattern(key);
contentMap.set(entryPattern.outputFiles.content, value);
});
metaMap.style = styleMap;
const config = new Map<
string,
{
component: true | undefined;
usingComponents: { selector: string; path: string }[];
existConfig: string;
}
>();
metaMap.useComponentPath.forEach((value, key) => {
const entryPattern = this.getComponentPagePattern(key);
const list = [
...value.libraryPath.map((item) => {
item.path = resolve(
normalize('/'),
join(normalize(LIBRARY_OUTPUT_ROOTDIR), item.path)
);
return item;
}),
];
list.push(
...value.localPath.map((item) => ({
selector: item.selector,
path: resolve(
normalize('/'),
normalize(this.getComponentPagePattern(item.path).outputFiles.path)
),
className: item.className,
}))
);
config.set(entryPattern.outputFiles.config, {
component: entryPattern.type === 'component' || undefined,
usingComponents: list,
existConfig: entryPattern.inputFiles.config,
});
});
for (const key in metaMap.otherMetaCollectionGroup) {
if (
Object.prototype.hasOwnProperty.call(
metaMap.otherMetaCollectionGroup,
key
)
) {
const element = metaMap.otherMetaCollectionGroup[key];
element.libraryPath.forEach((item) => {
item.path = resolve(
normalize('/'),
join(normalize(LIBRARY_OUTPUT_ROOTDIR), item.path)
);
});
element.localPath.forEach((item) => {
item.path = resolve(
normalize('/'),
normalize(this.getComponentPagePattern(item.path).outputFiles.path)
);
});
}
}
return {
style: styleMap,
outputContent: contentMap,
config: config,
otherMetaCollectionGroup: metaMap.otherMetaCollectionGroup,
selfTemplate,
};
}
private initHost(config: ParsedConfiguration) {
const host = ts.createIncrementalCompilerHost(config.options, this.system);
this.augmentResolveModuleNames(host, config.options);
this.addCleanDependency(host);
return host;
}
private async initTscProgram() {
const { readConfiguration, NgtscProgram } = await angularCompilerCliPromise;
const config = readConfiguration(this.tsConfig, undefined);
const host = this.initHost(config);
this.ngTscProgram = new NgtscProgram(
config.rootNames,
config.options,
host
);
this.tsProgram = this.ngTscProgram.getTsProgram();
this.typeChecker = this.tsProgram.getTypeChecker();
this.augmentProgramWithVersioning(this.tsProgram);
if (this.compiler.watchMode) {
this.builder = this.oldBuilder =
ts.createEmitAndSemanticDiagnosticsBuilderProgram(
this.tsProgram,
host,
this.oldBuilder
);
} else {
this.builder = ts.createAbstractBuilder(this.tsProgram, host);
}
this.ngCompiler = this.ngTscProgram.compiler;
}
/** 获得组件/页面的入口 */
private getComponentPagePattern(fileName: string) {
const findList = [fileName];
let maybeEntryPath: PagePattern | undefined;
while (findList.length) {
const module = findList.shift();
const moduleList = this.dependencyUseModule.get(path.normalize(module!));
if (moduleList && moduleList.length) {
findList.push(...moduleList);
} else {
maybeEntryPath = this.pagePatternList.find(
(item) => path.normalize(item.src) === path.normalize(module!)
);
if (maybeEntryPath) {
const sourceFile = this.tsProgram.getSourceFile(maybeEntryPath.src)!;
const selector = createCssSelectorForTs(sourceFile);
let importComponent: ts.Expression;
if (maybeEntryPath.type === 'page') {
const node = selector.queryOne(
`CallExpression[expression=pageStartup]`
) as ts.CallExpression;
importComponent = node.arguments[1];
} else {
const node = selector.queryOne(
`CallExpression[expression=componentRegistry]`
) as ts.CallExpression;
importComponent = node.arguments[0];
}
const symbol = this.typeChecker.getSymbolAtLocation(importComponent);
const node = symbol?.getDeclarations()?.[0];
const importDeclaration = node?.parent.parent
.parent as ts.ImportDeclaration;
const relativeImportComponentPath = importDeclaration.moduleSpecifier
.getText()
.slice(1, -1);
const importComponentPath =
path.resolve(
path.dirname(maybeEntryPath.src),
path.normalize(relativeImportComponentPath)
) + '.ts';
if (importComponentPath === path.normalize(fileName)) {
break;
}
maybeEntryPath = undefined;
}
}
}
if (!maybeEntryPath) {
throw new Error(`没有找到组件[${fileName}]对应的入口点`);
}
return maybeEntryPath;
}
private addCleanDependency(host: ts.CompilerHost) {
const oldReadFile = host.readFile;
const _this = this;
host.readFile = function (fileName) {
if (fileName.includes('node_modules')) {
_this.cleanDependencyFileCacheSet.add(externalizePath(fileName));
}
return oldReadFile.call(this, fileName);
};
}
private saveModuleDependency(
filePath: string,
moduleName: string,
module: ts.ResolvedModule
) {
if (!module) {
throw new Error(`模块未被解析,文件名${filePath},模块名${moduleName}`);
}
const useList =
this.dependencyUseModule.get(path.normalize(module.resolvedFileName)) ||
[];
useList.push(filePath);
this.dependencyUseModule.set(
path.normalize(module.resolvedFileName),
useList
);
}
private augmentResolveModuleNames(
host: ts.CompilerHost,
compilerOptions: CompilerOptions
) {
const moduleResolutionCache = ts.createModuleResolutionCache(
host.getCurrentDirectory(),
host.getCanonicalFileName.bind(host),
compilerOptions
);
const oldResolveModuleNames = host.resolveModuleNames;
if (oldResolveModuleNames) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
host.resolveModuleNames = (moduleNames: string[], ...args: any[]) => {
return moduleNames.map((name) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result = (oldResolveModuleNames! as any).call(
host,
[name],
...args
);
this.saveModuleDependency(args[0], name, result);
return result;
});
};
} else {
host.resolveModuleNames = (
moduleNames: string[],
containingFile: string,
_reusedNames: string[] | undefined,
redirectedReference: ts.ResolvedProjectReference | undefined,
options: ts.CompilerOptions
) => {
return moduleNames.map((name) => {
const result = ts.resolveModuleName(
name,
containingFile,
options,
host,
moduleResolutionCache,
redirectedReference
).resolvedModule;
if (!containingFile.includes('node_modules')) {
this.saveModuleDependency(containingFile, name, result!);
}
return result;
});
};
}
}
async analyzeAsync() {
await this.initTscProgram();
await this.ngCompiler.analyzeAsync();
}
getBuilder() {
return this.builder;
}
cleanDependencyFileCache() {
this.cleanDependencyFileCacheSet.forEach((filePath) => {
try {
this.compiler.inputFileSystem.purge!(filePath);
} catch (error) {}
});
}
private augmentProgramWithVersioning(program: ts.Program): void {
const baseGetSourceFiles = program.getSourceFiles;
program.getSourceFiles = function (...parameters) {
const files: readonly (ts.SourceFile & { version?: string })[] =
baseGetSourceFiles(...parameters);
for (const file of files) {
if (file.version === undefined) {
file.version = createHash('sha256').update(file.text).digest('hex');
}
}
return files;
};
}
}
================================================
FILE: src/builder/application/plugin/dynamic-library-entry.plugin.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { join, normalize } from '@angular-devkit/core';
import path from 'path';
import { Injectable } from 'static-injector';
import * as webpack from 'webpack';
import { LIBRARY_OUTPUT_ROOTDIR } from '../../library';
import type { LibraryComponentEntryMeta } from '../../library';
import { BuildPlatform } from '../../platform/platform';
import { LibrarySymbol } from '../const';
import type { LibraryLoaderContext } from '../type';
const CUSTOM_URI = 'dynamic';
const CUSTOM_URI_REG = /^dynamic:\/\/__license(?:\/|\\)(.*)\.ts$/;
@Injectable()
export class DynamicLibraryComponentEntryPlugin {
private libraryComponentMap = new Map();
constructor(private buildPlatform: BuildPlatform) {}
apply(compiler: webpack.Compiler) {
compiler.hooks.thisCompilation.tap(
'DynamicLibraryEntryPlugin',
(thisCompilation) => {
(thisCompilation as any)[LibrarySymbol] = (thisCompilation as any)[
LibrarySymbol
] || { buildPlatform: this.buildPlatform };
const hooks = webpack.NormalModule.getCompilationHooks(thisCompilation);
hooks.readResource
.for(CUSTOM_URI)
.tapAsync(
'DynamicLibraryEntryPlugin',
(loaderContext: any, callback) => {
const resourcePath: string = loaderContext.resourcePath;
const id = resourcePath.match(CUSTOM_URI_REG)![1];
const libraryMeta = this.libraryComponentMap.get(id);
callback(
undefined,
`
import * as amp from 'angular-miniprogram';
import * as library from '${libraryMeta?.contextPath}';
amp.componentRegistry(library.${libraryMeta?.className});
`
);
return;
}
);
compiler.hooks.finishMake.tapAsync(
'DynamicLibraryEntryPlugin',
(compilation, callback) => {
const libraryLoaderContext: LibraryLoaderContext = (
compilation as any
)[LibrarySymbol];
if (compilation !== thisCompilation) {
callback(undefined);
return;
}
if (libraryLoaderContext.libraryMetaList) {
libraryLoaderContext.libraryMetaList.forEach((item) => {
this.libraryComponentMap.set(item.id, item);
});
}
if (this.libraryComponentMap.size === 0) {
callback(undefined);
return;
}
let j = 0;
this.libraryComponentMap.forEach((meta) => {
const entry = join(
normalize(LIBRARY_OUTPUT_ROOTDIR),
meta.libraryPath
);
const dep = webpack.EntryPlugin.createDependency(
`${CUSTOM_URI}://${path.join('__license', meta.id)}.ts`,
entry
);
compilation.addEntry(
compiler.context,
dep,
entry,
(err, result) => {
j++;
if (j === this.libraryComponentMap.size) {
callback(undefined);
}
}
);
});
}
);
}
);
}
}
================================================
FILE: src/builder/application/plugin/dynamic-watch-entry.plugin.ts
================================================
import type { BuilderContext } from '@angular-devkit/architect';
import type { AssetPattern } from '@angular-devkit/build-angular';
import { normalizeAssetPatterns } from '@angular-devkit/build-angular/src/utils';
import { Path, getSystemPath, normalize, resolve } from '@angular-devkit/core';
import * as glob from 'glob';
import * as path from 'path';
import { BehaviorSubject, firstValueFrom } from 'rxjs';
import { filter, take } from 'rxjs/operators';
import { Injectable } from 'static-injector';
import * as webpack from 'webpack';
import { BuildPlatform } from '../../platform/platform';
import type { PagePattern } from '../type';
function globAsync(pattern: string, options: glob.IOptions) {
return new Promise((resolve, reject) =>
glob.default(pattern, options, (e, m) => (e ? reject(e) : resolve(m)))
);
}
@Injectable()
export class DynamicWatchEntryPlugin {
pageList!: PagePattern[];
componentList!: PagePattern[];
entryPattern$ = new BehaviorSubject<
| {
pageList: PagePattern[];
componentList: PagePattern[];
}
| undefined
>(undefined);
private first = true;
absoluteProjectRoot!: Path;
absoluteProjectSourceRoot!: Path;
constructor(
private options: {
pages: AssetPattern[];
components: AssetPattern[];
workspaceRoot: Path;
context: BuilderContext;
config: webpack.Configuration;
},
private buildPlatform: BuildPlatform
) {}
async init() {
const projectName =
this.options.context.target && this.options.context.target.project;
if (!projectName) {
throw new Error('The builder requires a target.');
}
const projectMetadata = await this.options.context.getProjectMetadata(
projectName
);
this.absoluteProjectRoot = normalize(
getSystemPath(
resolve(
this.options.workspaceRoot,
normalize((projectMetadata.root as string) || '')
)
)
);
const relativeSourceRoot = projectMetadata.sourceRoot as string | undefined;
const absoluteSourceRootPath =
typeof relativeSourceRoot === 'string'
? resolve(this.options.workspaceRoot, normalize(relativeSourceRoot))
: undefined;
if (relativeSourceRoot) {
this.absoluteProjectSourceRoot = normalize(
getSystemPath(absoluteSourceRootPath!)
)!;
}
}
apply(compiler: webpack.Compiler) {
let rootCompilation: boolean = false;
compiler.hooks.beforeCompile.tapPromise(
'DynamicWatchEntryPlugin',
async (compilationParams) => {
if (rootCompilation) {
return;
}
this.entryPattern$.next({
pageList: await this.generateModuleInfo(
this.options.pages || [],
'page'
),
componentList: await this.generateModuleInfo(
this.options.components || [],
'component'
),
});
}
);
compiler.hooks.thisCompilation.tap(
'DynamicWatchEntryPlugin',
(compilation) => {
rootCompilation = true;
if (this.first) {
this.first = false;
const patternList = normalizeAssetPatterns(
[...(this.options.pages || []), ...(this.options.components || [])],
this.options.workspaceRoot,
this.absoluteProjectRoot,
this.absoluteProjectSourceRoot
);
for (const pattern of patternList) {
const cwd = path.resolve(
this.options.context.workspaceRoot,
pattern.input
);
compilation.fileDependencies.add(cwd);
}
}
}
);
// 因为监听更新的时候beforeCompile会拦截所有的,所以这么实现(因为还有一次性构建不触发watchRun,所以不能代替)
compiler.hooks.watchRun.tap('DynamicWatchEntryPlugin', async () => {
rootCompilation = false;
});
// 入口移动到这里是因为ng新增了一个插件也同时修改了入口,
compiler.hooks.environment.tap(
{ name: `DynamicWatchEntryPlugin`, stage: 9999 },
() => {
const originEntryConfig = compiler.options.entry;
compiler.options.entry = async () => {
await firstValueFrom(
this.entryPattern$.pipe(filter(Boolean), take(1))
);
const entryPattern = this.entryPattern$.value!;
const list = [
...entryPattern.pageList,
...entryPattern.componentList,
];
const result = (await (typeof originEntryConfig === 'function'
? originEntryConfig()
: originEntryConfig))!;
if (result['app']) {
throw new Error(
'资源文件不能指定为app文件名或bundleName,请重新修改(不影响导出)'
);
}
return {
...result,
...list.reduce((pre, cur) => {
pre[cur.entryName] = { import: [cur.src] };
return pre;
}, {} as Record[string]>),
};
};
}
);
}
private async generateModuleInfo(
list: AssetPattern[],
type: 'page' | 'component'
) {
const patternList = normalizeAssetPatterns(
list,
this.options.workspaceRoot,
this.absoluteProjectRoot,
this.absoluteProjectSourceRoot
);
const moduleList: PagePattern[] = [];
for (const pattern of patternList) {
const cwd = path.resolve(
this.options.context.workspaceRoot,
pattern.input
);
/** 当前匹配匹配到的文件 */
const files = await globAsync(pattern.glob, {
cwd,
dot: true,
nodir: true,
ignore: pattern.ignore || [],
follow: pattern.followSymlinks,
});
moduleList.push(
...files.map((file) => {
const object: Partial = {
entryName: path.basename(file, '.ts').replace(/\./g, '-'),
fileName: file,
src: path.join(cwd, file),
...pattern,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
outputFiles: {} as any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
inputFiles: {} as any,
};
object.inputFiles!.config = object.src!.replace(
/\.ts$/,
this.buildPlatform.fileExtname.config!
);
const outputFileName =
object.fileName!.replace(/\.ts$/, '').replace(/\./g, '-') + '.ts';
object.outputFiles!.path = path
.join(pattern.output, outputFileName)
.replace(/\.ts$/, '');
object.outputFiles!.logic =
object.outputFiles!.path + this.buildPlatform.fileExtname.logic;
object.outputFiles!.style =
object.outputFiles!.path + this.buildPlatform.fileExtname.style;
object.outputFiles!.content =
object.outputFiles!.path + this.buildPlatform.fileExtname.content;
object.outputFiles!.config =
object.outputFiles!.path + this.buildPlatform.fileExtname.config;
object.type = type;
return object as PagePattern;
})
);
}
return moduleList;
}
}
================================================
FILE: src/builder/application/plugin/export-mini-program-assets.plugin.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
/* eslint-disable no-console */
import { normalizePath } from '@ngtools/webpack/src/ivy/paths';
import {
InputFileSystemSync,
createWebpackSystem,
} from '@ngtools/webpack/src/ivy/system';
import * as path from 'path';
import { Inject, Injectable, Injector } from 'static-injector';
import ts from 'typescript';
import { sources } from 'webpack';
import * as webpack from 'webpack';
import { BuildPlatform } from '../../platform/platform';
import { literalResolve } from '../../util';
import { ExportMiniProgramAssetsPluginSymbol, InjectorSymbol } from '../const';
import { LibraryTemplateScopeService } from '../library-template-scope.service';
import type { ComponentTemplateLoaderContext } from '../loader/type';
import { MiniProgramApplicationAnalysisService } from '../mini-program-application-analysis.service';
import {
OLD_BUILDER,
PAGE_PATTERN_TOKEN,
TS_CONFIG_TOKEN,
TS_SYSTEM,
WEBPACK_COMPILATION,
WEBPACK_COMPILER,
} from '../token';
import type { PagePattern } from '../type';
import { LibraryTemplateLiteralConvertOptions } from '../type';
import { setCompilationAsset } from '../util';
@Injectable()
export class ExportMiniProgramAssetsPlugin {
private pageList!: PagePattern[];
private componentList!: PagePattern[];
private system!: ts.System;
constructor(
@Inject(TS_CONFIG_TOKEN) tsConfig: string,
private buildPlatform: BuildPlatform,
private injector: Injector,
private libraryTemplateScopeService: LibraryTemplateScopeService
) {}
apply(compiler: webpack.Compiler) {
const ifs = compiler.inputFileSystem as InputFileSystemSync;
let oldBuilder: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined =
undefined;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const styleAssets = new Map();
compiler.hooks.compilation.tap(
'ExportMiniProgramAssetsPlugin',
(compilation) => {
compilation.hooks.processAssets.tap(
'ExportMiniProgramAssetsPlugin',
() => {
for (const stylePath in compilation.assets) {
if (
Object.prototype.hasOwnProperty.call(
compilation.assets,
stylePath
)
) {
const data = compilation.getAsset(stylePath)!;
if (/\.(scss|css|sass|less|styl)$/.test(stylePath)) {
styleAssets.set(path.normalize(stylePath), data.source);
setCompilationAsset(
compilation,
stylePath,
new sources.RawSource(' ')
);
}
}
}
}
);
}
);
compiler.hooks.thisCompilation.tap(
'ExportMiniProgramAssetsPlugin',
(compilation) => {
this.system = createWebpackSystem(
compiler.inputFileSystem as InputFileSystemSync,
normalizePath(compiler.context)
);
this.libraryTemplateScopeService.register(compilation);
(compilation as any)[InjectorSymbol] = this.injector;
const injector = Injector.create({
providers: [
{ provide: MiniProgramApplicationAnalysisService },
{ provide: WEBPACK_COMPILATION, useValue: compilation },
{ provide: WEBPACK_COMPILER, useValue: compiler },
{ provide: OLD_BUILDER, useValue: oldBuilder },
{
provide: TS_SYSTEM,
useValue: this.system,
},
{
provide: PAGE_PATTERN_TOKEN,
useValue: [...this.pageList, ...this.componentList],
},
],
parent: this.injector,
});
const templateService = injector.get(
MiniProgramApplicationAnalysisService
);
oldBuilder =
templateService.getBuilder() as ts.EmitAndSemanticDiagnosticsBuilderProgram;
const buildTemplatePromise = this.buildTemplate(templateService);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(compilation as any)[ExportMiniProgramAssetsPluginSymbol] = {
buildPlatform: this.buildPlatform,
otherMetaGroupPromise: buildTemplatePromise.then(
(item) => item.otherMetaCollectionGroup
),
} as ComponentTemplateLoaderContext;
compilation.hooks.processAssets.tapAsync(
'ExportMiniProgramAssetsPlugin',
async (assets, cb) => {
const metaMap = await buildTemplatePromise;
metaMap.outputContent.forEach((value, key) => {
setCompilationAsset(
compilation,
key,
new sources.RawSource(value)
);
});
metaMap.style.forEach((value, outputPath) => {
setCompilationAsset(
compilation,
outputPath,
new sources.ConcatSource(
...value.map((item) => styleAssets.get(item)!)
)
);
});
metaMap.config.forEach((value, key) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let config: Record;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if ((ifs as any).fileSystem.existsSync(value.existConfig)) {
config = JSON.parse(
ifs.readFileSync(value.existConfig).toString()
);
} else {
config = {};
}
config.component ??= value.component;
config.usingComponents = config.usingComponents || {};
config.usingComponents = {
...config.usingComponents,
...value.usingComponents.reduce((pre, cur) => {
pre[cur.selector] = cur.path;
return pre;
}, {} as Record),
};
setCompilationAsset(
compilation,
key,
new sources.RawSource(JSON.stringify(config))
);
});
for (const key in metaMap.otherMetaCollectionGroup) {
if (
Object.prototype.hasOwnProperty.call(
metaMap.otherMetaCollectionGroup,
key
)
) {
const element = metaMap.otherMetaCollectionGroup[key];
this.libraryTemplateScopeService.setScopeExtraUseComponents(
key,
{
useComponents: {
...[...element.localPath, ...element.libraryPath].reduce(
(pre, cur) => {
pre[cur.selector] = cur.path;
return pre;
},
{} as Record
),
},
templateList: element.templateList.map(
(item) => item.content
),
}
);
}
}
const componentConfigGroup =
this.libraryTemplateScopeService.exportLibraryComponentConfig();
for (const item of componentConfigGroup) {
setCompilationAsset(
compilation,
item.filePath,
new sources.RawSource(JSON.stringify(item.content))
);
}
const templateGroup =
this.libraryTemplateScopeService.exportLibraryTemplate();
for (const key in templateGroup) {
if (Object.prototype.hasOwnProperty.call(templateGroup, key)) {
const element = templateGroup[key];
setCompilationAsset(
compilation,
key,
new sources.RawSource(
literalResolve(
`\`${element}\``,
{
directivePrefix:
this.buildPlatform.templateTransform.getData()
.directivePrefix,
eventListConvert:
this.buildPlatform.templateTransform.eventListConvert,
templateInterpolation:
this.buildPlatform.templateTransform
.templateInterpolation,
fileExtname: this.buildPlatform.fileExtname,
}
)
)
);
}
}
for (const key in metaMap.selfTemplate) {
if (
Object.prototype.hasOwnProperty.call(metaMap.selfTemplate, key)
) {
const element = metaMap.selfTemplate[key];
setCompilationAsset(
compilation,
key,
new sources.RawSource(element)
);
}
}
cb();
}
);
templateService.cleanDependencyFileCache();
}
);
}
public setEntry(pageList: PagePattern[], componentList: PagePattern[]) {
this.pageList = pageList;
this.componentList = componentList;
}
async buildTemplate(service: MiniProgramApplicationAnalysisService) {
try {
await service.analyzeAsync();
const result = await service.exportComponentBuildMetaMap();
return result;
} catch (error) {
console.error(error);
throw error;
}
}
}
================================================
FILE: src/builder/application/schema.base.json
================================================
{
"$schema": "http://json-schema.org/schema",
"title": "Webpack browser schema for Build Facade.",
"description": "Browser target options",
"type": "object",
"properties": {
"pages": {
"type": "array",
"description": "页面配置",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"components": {
"type": "array",
"description": "组件配置",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"platform": {
"type": "string",
"description": "小程序平台",
"default": "wx"
}
},
"definitions": {
"assetPattern": {
"oneOf": [
{
"type": "object",
"properties": {
"followSymlinks": {
"type": "boolean",
"default": false,
"description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched."
},
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply 'glob'. Defaults to the project root."
},
"ignore": {
"description": "An array of globs to ignore.",
"type": "array",
"items": {
"type": "string"
}
},
"output": {
"type": "string",
"description": "Absolute path within the output."
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{
"type": "string"
}
]
}
},
"additionalProperties": false
}
================================================
FILE: src/builder/application/schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Webpack browser schema for Build Facade.",
"description": "Browser target options",
"type": "object",
"properties": {
"pages": {
"type": "array",
"description": "页面配置",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"components": {
"type": "array",
"description": "组件配置",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"platform": {
"type": "string",
"description": "小程序平台",
"default": "wx"
},
"assets": {
"type": "array",
"description": "List of static application assets.",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"main": {
"type": "string",
"description": "The full path for the main entry point to the app, relative to the current workspace."
},
"polyfills": {
"type": "string",
"description": "The full path for the polyfills file, relative to the current workspace."
},
"tsConfig": {
"type": "string",
"description": "The full path for the TypeScript configuration file, relative to the current workspace."
},
"scripts": {
"description": "Global scripts to be included in the build.",
"type": "array",
"default": [],
"items": {
"$ref": "#/definitions/extraEntryPoint"
}
},
"styles": {
"description": "Global styles to be included in the build.",
"type": "array",
"default": [],
"items": {
"$ref": "#/definitions/extraEntryPoint"
}
},
"inlineStyleLanguage": {
"description": "The stylesheet language to use for the application's inline component styles.",
"type": "string",
"default": "css",
"enum": ["css", "less", "sass", "scss"]
},
"stylePreprocessorOptions": {
"description": "Options to pass to style preprocessors.",
"type": "object",
"properties": {
"includePaths": {
"description": "Paths to include. Paths will be resolved to workspace root.",
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"additionalProperties": false
},
"optimization": {
"description": "Enables optimization of the build output. Including minification of scripts and styles, tree-shaking, dead-code elimination, inlining of critical CSS and fonts inlining. For more information, see https://angular.io/guide/workspace-config#optimization-configuration.",
"x-user-analytics": 16,
"default": true,
"oneOf": [
{
"type": "object",
"properties": {
"scripts": {
"type": "boolean",
"description": "Enables optimization of the scripts output.",
"default": true
},
"styles": {
"description": "Enables optimization of the styles output.",
"default": true,
"oneOf": [
{
"type": "object",
"properties": {
"minify": {
"type": "boolean",
"description": "Minify CSS definitions by removing extraneous whitespace and comments, merging identifiers and minimizing values.",
"default": true
},
"inlineCritical": {
"type": "boolean",
"description": "Extract and inline critical CSS definitions to improve first paint time.",
"default": true
}
},
"additionalProperties": false
},
{
"type": "boolean"
}
]
},
"fonts": {
"description": "Enables optimization for fonts. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.",
"default": true,
"oneOf": [
{
"type": "object",
"properties": {
"inline": {
"type": "boolean",
"description": "Reduce render blocking requests by inlining external Google fonts and icons CSS definitions in the application's HTML index file. This option requires internet access. `HTTPS_PROXY` environment variable can be used to specify a proxy server.",
"default": true
}
},
"additionalProperties": false
},
{
"type": "boolean"
}
]
}
},
"additionalProperties": false
},
{
"type": "boolean"
}
]
},
"fileReplacements": {
"description": "Replace compilation source files with other compilation source files in the build.",
"type": "array",
"items": {
"$ref": "#/definitions/fileReplacement"
},
"default": []
},
"outputPath": {
"type": "string",
"description": "The full path for the new output directory, relative to the current workspace.\n\nBy default, writes output to a folder named dist/ in the current project."
},
"resourcesOutputPath": {
"type": "string",
"description": "The path where style resources will be placed, relative to outputPath.",
"default": ""
},
"aot": {
"type": "boolean",
"description": "Build using Ahead of Time compilation.",
"x-user-analytics": 13,
"default": true
},
"sourceMap": {
"description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": false,
"oneOf": [
{
"type": "object",
"properties": {
"scripts": {
"type": "boolean",
"description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
"description": "Output source maps for all styles.",
"default": true
},
"hidden": {
"type": "boolean",
"description": "Output source maps used for error reporting tools.",
"default": false
},
"vendor": {
"type": "boolean",
"description": "Resolve vendor packages source maps.",
"default": false
}
},
"additionalProperties": false
},
{
"type": "boolean"
}
]
},
"vendorChunk": {
"type": "boolean",
"description": "Generate a seperate bundle containing only vendor libraries. This option should only used for development.",
"default": false
},
"commonChunk": {
"type": "boolean",
"description": "Generate a seperate bundle containing code used across multiple bundles.",
"default": true
},
"baseHref": {
"type": "string",
"description": "Base url for the application being built."
},
"deployUrl": {
"type": "string",
"description": "URL where files will be deployed."
},
"verbose": {
"type": "boolean",
"description": "Adds more details to output logging.",
"default": false
},
"progress": {
"type": "boolean",
"description": "Log progress to the console while building.",
"default": true
},
"i18nMissingTranslation": {
"type": "string",
"description": "How to handle missing translations for i18n.",
"enum": ["warning", "error", "ignore"],
"default": "warning"
},
"localize": {
"description": "Translate the bundles in one or more locales.",
"oneOf": [
{
"type": "boolean",
"description": "Translate all locales."
},
{
"type": "array",
"description": "List of locales ID's to translate.",
"minItems": 1,
"items": {
"type": "string",
"pattern": "^[a-zA-Z]{2,3}(-[a-zA-Z]{4})?(-([a-zA-Z]{2}|[0-9]{3}))?(-[a-zA-Z]{5,8})?(-x(-[a-zA-Z0-9]{1,8})+)?$"
}
}
]
},
"extractCss": {
"type": "boolean",
"description": "Extract CSS from global styles into '.css' files instead of '.js'.",
"default": true,
"x-deprecated": "Deprecated since version 11.0. No longer required to disable CSS extraction for HMR."
},
"watch": {
"type": "boolean",
"description": "Run build when files change.",
"default": false
},
"outputHashing": {
"type": "string",
"description": "Define the output filename cache-busting hashing mode.",
"default": "none",
"enum": ["none", "all", "media", "bundles"]
},
"poll": {
"type": "number",
"description": "Enable and define the file watching poll time period in milliseconds."
},
"deleteOutputPath": {
"type": "boolean",
"description": "Delete the output path before building.",
"default": true
},
"preserveSymlinks": {
"type": "boolean",
"description": "Do not use the real path when resolving modules. If unset then will default to `true` if NodeJS option --preserve-symlinks is set."
},
"extractLicenses": {
"type": "boolean",
"description": "Extract all licenses in a separate file.",
"default": true
},
"showCircularDependencies": {
"type": "boolean",
"description": "Show circular dependency warnings on builds.",
"default": false,
"x-deprecated": "The recommended method to detect circular dependencies in project code is to use either a lint rule or other external tooling."
},
"buildOptimizer": {
"type": "boolean",
"description": "Enables '@angular-devkit/build-optimizer' optimizations when using the 'aot' option.",
"default": true
},
"namedChunks": {
"type": "boolean",
"description": "Use file name for lazy loaded chunks.",
"default": false
},
"subresourceIntegrity": {
"type": "boolean",
"description": "Enables the use of subresource integrity validation.",
"default": false
},
"serviceWorker": {
"type": "boolean",
"description": "Generates a service worker config for production builds.",
"default": false
},
"ngswConfigPath": {
"type": "string",
"description": "Path to ngsw-config.json."
},
"index": {
"description": "Configures the generation of the application's HTML index.",
"oneOf": [
{
"type": "string",
"description": "The path of a file to use for the application's HTML index. The filename of the specified path will be used for the generated file and will be created in the root of the application's configured output path."
},
{
"type": "object",
"description": "",
"properties": {
"input": {
"type": "string",
"minLength": 1,
"description": "The path of a file to use for the application's generated HTML index."
},
"output": {
"type": "string",
"minLength": 1,
"default": "index.html",
"description": "The output path of the application's generated HTML index file. The full provided path will be used and will be considered relative to the application's configured output path."
}
},
"required": ["input"]
}
]
},
"statsJson": {
"type": "boolean",
"description": "Generates a 'stats.json' file which can be analyzed using tools such as 'webpack-bundle-analyzer'.",
"default": false
},
"budgets": {
"description": "Budget thresholds to ensure parts of your application stay within boundaries which you set.",
"type": "array",
"items": {
"$ref": "#/definitions/budget"
},
"default": []
},
"webWorkerTsConfig": {
"type": "string",
"description": "TypeScript configuration for Web Worker modules."
},
"crossOrigin": {
"type": "string",
"description": "Define the crossorigin attribute setting of elements that provide CORS support.",
"default": "none",
"enum": ["none", "anonymous", "use-credentials"]
},
"allowedCommonJsDependencies": {
"description": "A list of CommonJS packages that are allowed to be used without a build time warning.",
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"additionalProperties": false,
"required": ["outputPath", "index", "main", "tsConfig"],
"definitions": {
"assetPattern": {
"oneOf": [
{
"type": "object",
"properties": {
"followSymlinks": {
"type": "boolean",
"default": false,
"description": "Allow glob patterns to follow symlink directories. This allows subdirectories of the symlink to be searched."
},
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply 'glob'. Defaults to the project root."
},
"ignore": {
"description": "An array of globs to ignore.",
"type": "array",
"items": {
"type": "string"
}
},
"output": {
"type": "string",
"description": "Absolute path within the output."
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{
"type": "string"
}
]
},
"fileReplacement": {
"oneOf": [
{
"type": "object",
"properties": {
"src": {
"type": "string",
"pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"replaceWith": {
"type": "string",
"pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
"required": ["src", "replaceWith"]
},
{
"type": "object",
"properties": {
"replace": {
"type": "string",
"pattern": "\\.(([cm]?j|t)sx?|json)$"
},
"with": {
"type": "string",
"pattern": "\\.(([cm]?j|t)sx?|json)$"
}
},
"additionalProperties": false,
"required": ["replace", "with"]
}
]
},
"extraEntryPoint": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include."
},
"bundleName": {
"type": "string",
"pattern": "^[\\w\\-.]*$",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include."
}
]
},
"budget": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "The type of budget.",
"enum": [
"all",
"allScript",
"any",
"anyScript",
"anyComponentStyle",
"bundle",
"initial"
]
},
"name": {
"type": "string",
"description": "The name of the bundle."
},
"baseline": {
"type": "string",
"description": "The baseline size for comparison."
},
"maximumWarning": {
"type": "string",
"description": "The maximum threshold for warning relative to the baseline."
},
"maximumError": {
"type": "string",
"description": "The maximum threshold for error relative to the baseline."
},
"minimumWarning": {
"type": "string",
"description": "The minimum threshold for warning relative to the baseline."
},
"minimumError": {
"type": "string",
"description": "The minimum threshold for error relative to the baseline."
},
"warning": {
"type": "string",
"description": "The threshold for warning relative to the baseline (min & max)."
},
"error": {
"type": "string",
"description": "The threshold for error relative to the baseline (min & max)."
}
},
"additionalProperties": false,
"required": ["type"]
}
}
}
================================================
FILE: src/builder/application/token.ts
================================================
import { InjectionToken } from 'static-injector';
export const TS_CONFIG_TOKEN = new InjectionToken('TS_CONFIG_TOKEN');
export const PAGE_PATTERN_TOKEN = new InjectionToken('PAGE_PATTERN_TOKEN');
export const OLD_BUILDER = new InjectionToken('OLD_BUILDER');
export const TS_SYSTEM = new InjectionToken('TS_SYSTEM');
export const WEBPACK_COMPILATION = new InjectionToken('WEBPACK_COMPILATION');
export const WEBPACK_COMPILER = new InjectionToken('WEBPACK_COMPILER');
================================================
FILE: src/builder/application/type.ts
================================================
import type { AssetPattern } from '@angular-devkit/build-angular';
import { LibraryComponentEntryMeta } from '../library';
import { BuildPlatform, PlatformFileExtname } from '../platform';
export interface LibraryTemplateLiteralConvertOptions {
directivePrefix: string;
eventListConvert: (name: string[]) => string;
templateInterpolation: [string, string];
fileExtname: PlatformFileExtname;
}
export interface PagePattern extends Exclude {
/** 入口名 */
entryName: string;
/** 匹配文件,相对于input */
fileName: string;
/** 要输出的js出口 */
output: string;
/** 绝对路径,path.join */
src: string;
outputFiles: {
content: string;
style: string;
logic: string;
path: string;
config: string;
};
inputFiles: {
config: string;
};
type: 'component' | 'page';
}
export interface LibraryLoaderContext {
libraryMetaList: LibraryComponentEntryMeta[];
buildPlatform: BuildPlatform;
}
================================================
FILE: src/builder/application/util/index.ts
================================================
export * from './set-compilation-asset';
================================================
FILE: src/builder/application/util/set-compilation-asset.ts
================================================
import * as webpack from 'webpack';
export function setCompilationAsset(
compilation: webpack.Compilation,
key: string,
content: webpack.sources.Source
) {
if (compilation.getAsset(key)) {
compilation.updateAsset(key, content, {});
} else {
compilation.emitAsset(key, content, {});
}
}
================================================
FILE: src/builder/application/webpack-configuration-change.service.ts
================================================
import type { BuilderContext } from '@angular-devkit/architect';
import {
AssetPattern,
BrowserBuilderOptions,
KarmaBuilderOptions,
} from '@angular-devkit/build-angular';
import { normalize } from '@angular-devkit/core';
import * as path from 'path';
import { filter } from 'rxjs/operators';
import { Injectable, Injector } from 'static-injector';
import * as webpack from 'webpack';
import { DefinePlugin } from 'webpack';
import { BootstrapAssetsPlugin } from 'webpack-bootstrap-assets-plugin';
import { LIBRARY_OUTPUT_ROOTDIR } from '../library';
import { BuildPlatform } from '../platform/platform';
import type { PlatformType } from '../platform/platform';
import { LibraryTemplateScopeService } from './library-template-scope.service';
import { DynamicLibraryComponentEntryPlugin } from './plugin/dynamic-library-entry.plugin';
import { DynamicWatchEntryPlugin } from './plugin/dynamic-watch-entry.plugin';
import { ExportMiniProgramAssetsPlugin } from './plugin/export-mini-program-assets.plugin';
import { TS_CONFIG_TOKEN } from './token';
import type { PagePattern } from './type';
type OptimizationOptions = NonNullable;
type OptimizationSplitChunksOptions = Exclude<
OptimizationOptions['splitChunks'],
false | undefined
>;
type OptimizationSplitChunksCacheGroup = Exclude<
NonNullable[''],
false | string | Function | RegExp
>;
@Injectable()
export class WebpackConfigurationChangeService {
exportMiniProgramAssetsPluginInstance!: ExportMiniProgramAssetsPlugin;
private buildPlatform!: BuildPlatform;
private entryList!: PagePattern[];
constructor(
private options: (BrowserBuilderOptions | KarmaBuilderOptions) & {
pages: AssetPattern[];
components: AssetPattern[];
platform: PlatformType;
},
private context: BuilderContext,
private config: webpack.Configuration,
private injector: Injector
) {}
init() {
this.injector = Injector.create({
parent: this.injector,
providers: [
{ provide: ExportMiniProgramAssetsPlugin },
{ provide: LibraryTemplateScopeService },
{
provide: TS_CONFIG_TOKEN,
useValue: path.resolve(
this.context.workspaceRoot,
this.options.tsConfig
),
},
{
provide: DynamicWatchEntryPlugin,
deps: [BuildPlatform],
useFactory: (buildPlatform: BuildPlatform) => {
return new DynamicWatchEntryPlugin(
{
pages: this.options.pages,
components: this.options.components,
workspaceRoot: normalize(this.context.workspaceRoot),
context: this.context,
config: this.config,
},
buildPlatform
);
},
},
{ provide: DynamicLibraryComponentEntryPlugin },
],
});
this.buildPlatform = this.injector.get(BuildPlatform);
this.buildPlatform.fileExtname.config =
this.buildPlatform.fileExtname.config || '.json';
this.config.output!.globalObject = this.buildPlatform.globalObject;
}
async change() {
this.buildPlatformCompatible();
this.exportAssets();
await this.pageHandle();
this.addLoader();
this.globalVariableChange();
this.changeStylesExportSuffix();
this.config.plugins?.push(
this.injector.get(DynamicLibraryComponentEntryPlugin)
);
this.config.plugins?.push(
new webpack.NormalModuleReplacementPlugin(
/^angular-miniprogram\/platform\/wx$/,
`angular-miniprogram/platform/${this.buildPlatform.packageName}`
)
);
}
private buildPlatformCompatible() {
if (this.buildPlatform.packageName == 'zfb') {
this.config.resolve?.conditionNames?.shift();
this.config.resolve?.mainFields?.shift();
}
}
private async pageHandle() {
const dynamicWatchEntryInstance = this.injector.get(
DynamicWatchEntryPlugin
);
await dynamicWatchEntryInstance.init();
dynamicWatchEntryInstance.entryPattern$
.pipe(filter((item) => !!item))
.subscribe((result) => {
this.entryList = [...result!.pageList, ...result!.componentList];
this.exportMiniProgramAssetsPluginInstance.setEntry(
result!.pageList,
result!.componentList
);
});
this.config.plugins?.push(dynamicWatchEntryInstance);
// 出口
const oldFileName = this.config.output!.filename as string;
this.config.output!.filename = (chunkData) => {
const page = this.entryList.find(
(item) => item.entryName === chunkData.chunk!.name
);
if (page) {
return page.outputFiles.logic;
}
return oldFileName;
};
// 共享依赖
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const oldChunks = (this.config.optimization!.splitChunks as any).cacheGroups
.defaultVendors.chunks;
(
(
this.config.optimization!
.splitChunks! as unknown as OptimizationSplitChunksOptions
).cacheGroups!.defaultVendors as OptimizationSplitChunksCacheGroup
).chunks = (chunk) => {
if (
this.entryList.find((item) => item.entryName === chunk.name) ||
chunk.name!.startsWith(`${LIBRARY_OUTPUT_ROOTDIR}/`)
) {
return true;
}
return oldChunks(chunk);
};
((this.config.optimization!.splitChunks as OptimizationSplitChunksOptions)
.cacheGroups!['moduleChunks'] as OptimizationSplitChunksCacheGroup) = {
test: (module: webpack.NormalModule) => {
const name = module.nameForCondition();
return (
(name &&
name.endsWith('.ts') &&
!/[\\/]node_modules[\\/]/.test(name)) ||
name?.includes('angular-miniprogram\\dist')
);
},
minChunks: 2,
minSize: 0,
name: 'module-chunk',
chunks: 'all',
};
// 出口保留必要加载
const assetsPlugin = new BootstrapAssetsPlugin();
assetsPlugin.hooks.removeChunk.tap('pageHandle', (chunk) => {
if (
this.entryList.some((page) => page.entryName === chunk.name) ||
[...chunk.files].some((file) =>
file.endsWith(this.buildPlatform.fileExtname.style)
) ||
chunk.name!.startsWith(`${LIBRARY_OUTPUT_ROOTDIR}/`)
) {
return true;
}
return false;
});
assetsPlugin.hooks.emitAssets.tap('pageHandle', (object, json) => {
return {
'app.js':
this.buildPlatform.importTemplate +
json.scripts.map((item) => `require('./${item.src}')`).join(';'),
};
});
this.config.plugins!.push(assetsPlugin);
}
private exportAssets() {
this.exportMiniProgramAssetsPluginInstance = this.injector.get(
ExportMiniProgramAssetsPlugin
);
this.config.plugins!.unshift(this.exportMiniProgramAssetsPluginInstance);
}
private addLoader() {
this.config.module!.rules!.unshift({
test: /\.ts$/,
loader: require.resolve(
path.join(__dirname, './loader/component-template.loader')
),
});
this.config.module?.rules?.unshift({
test: /\.mjs$/,
loader: require.resolve(path.join(__dirname, './loader/library.loader')),
});
this.config.module?.rules?.unshift({
test: /\.mjs$/,
loader: require.resolve(
path.join(__dirname, './loader/library-template.loader')
),
});
}
private globalVariableChange() {
const defineObject: Record = {
global: `${this.buildPlatform.globalObject}.__global`,
window: `${this.buildPlatform.globalVariablePrefix}`,
globalThis: `${this.buildPlatform.globalVariablePrefix}`,
Zone: `${this.buildPlatform.globalVariablePrefix}.Zone`,
setTimeout: `${this.buildPlatform.globalVariablePrefix}.setTimeout`,
clearTimeout: `${this.buildPlatform.globalVariablePrefix}.clearTimeout`,
setInterval: `${this.buildPlatform.globalVariablePrefix}.setInterval`,
clearInterval: `${this.buildPlatform.globalVariablePrefix}.clearInterval`,
Promise: `${this.buildPlatform.globalVariablePrefix}.Promise`,
Reflect: `${this.buildPlatform.globalVariablePrefix}.Reflect`,
requestAnimationFrame: `${this.buildPlatform.globalVariablePrefix}.requestAnimationFrame`,
cancelAnimationFrame: `${this.buildPlatform.globalVariablePrefix}.cancelAnimationFrame`,
performance: `${this.buildPlatform.globalVariablePrefix}.performance`,
navigator: `${this.buildPlatform.globalVariablePrefix}.navigator`,
wx: this.buildPlatform.globalObject,
miniProgramPlatform: `"${this.buildPlatform.globalObject}"`,
queueMicrotask:`${this.buildPlatform.globalVariablePrefix}.queueMicrotask`
};
if (this.config.mode === 'development') {
defineObject[
'ngDevMode'
] = `${this.buildPlatform.globalObject}.__global.ngDevMode`;
}
this.config.plugins!.push(new DefinePlugin(defineObject));
}
private changeStylesExportSuffix() {
const index = this.config.plugins!.findIndex(
(plugin) =>
Object.getPrototypeOf(plugin).constructor.name ===
'MiniCssExtractPlugin'
);
if (index > -1) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const pluginInstance = this.config.plugins![index] as any;
const pluginPrototype = Object.getPrototypeOf(pluginInstance);
this.config.plugins?.splice(
index,
1,
new pluginPrototype.constructor({
filename: 'app' + this.buildPlatform.fileExtname.style,
})
);
} else {
throw new Error('没有找到MiniCssExtractPlugin插件,无法修改生成style');
}
}
}
================================================
FILE: src/builder/builder.prod.spec.ts
================================================
import { join, normalize } from '@angular-devkit/core';
import {
MyTestProjectHost,
describeBuilder,
setWorkspaceRoot,
} from '../../test/plugin-describe-builder';
import {
BROWSER_BUILDER_INFO,
DEFAULT_ANGULAR_CONFIG,
} from '../../test/test-builder';
import {
ALL_COMPONENT_NAME_LIST,
ALL_PAGE_NAME_LIST,
} from '../../test/util/file';
import { runBuilder } from './application';
import { PlatformType } from './platform/platform';
const angularConfig = {
...DEFAULT_ANGULAR_CONFIG,
platform: PlatformType.wx,
buildOptimizer: true,
optimization: true,
extractLicenses: true,
};
describeBuilder(runBuilder, BROWSER_BUILDER_INFO, (harness) => {
describe('builder-prod', () => {
it('运行', async () => {
const root = harness.host.root();
const myTestProjectHost = new MyTestProjectHost(harness.host);
const list = await myTestProjectHost.getFileList(
normalize(join(root, 'src', '__pages'))
);
list.push(
...(await myTestProjectHost.getFileList(
normalize(join(root, 'src', '__components'))
))
);
await myTestProjectHost.importPathRename(list);
await myTestProjectHost.moveDir(ALL_PAGE_NAME_LIST, '__pages', 'pages');
await myTestProjectHost.moveDir(
ALL_COMPONENT_NAME_LIST,
'__components',
'components'
);
await myTestProjectHost.addPageEntry(ALL_PAGE_NAME_LIST);
harness.useTarget('build', angularConfig);
const result = await harness.executeOnce();
expect(result).toBeTruthy();
expect(result.error).toBeFalsy();
expect(result.logs[0].level !== 'error').toBeTruthy();
expect(result.result?.success).toBeTruthy();
});
});
});
================================================
FILE: src/builder/builder.spec.ts
================================================
import { join, normalize } from '@angular-devkit/core';
import * as fs from 'fs-extra';
import * as path from 'path';
import { Injector } from 'static-injector';
import {
MyTestProjectHost,
describeBuilder,
setWorkspaceRoot,
} from '../../test/plugin-describe-builder';
import {
BROWSER_BUILDER_INFO,
DEFAULT_ANGULAR_CONFIG,
} from '../../test/test-builder';
import {
ALL_COMPONENT_NAME_LIST,
ALL_PAGE_NAME_LIST,
TEST_LIBRARY_COMPONENT_LIST,
} from '../../test/util/file';
import { runBuilder } from './application';
import { LIBRARY_OUTPUT_ROOTDIR } from './library';
import { BuildPlatform, PlatformType } from './platform/platform';
import { getBuildPlatformInjectConfig } from './platform/platform-inject-config';
const angularConfig = {
...DEFAULT_ANGULAR_CONFIG,
platform: PlatformType.wx,
sourceMap: false,
// buildOptimizer: true,
// optimization: true,
};
describeBuilder(runBuilder, BROWSER_BUILDER_INFO, (harness) => {
describe('builder-dev', () => {
for (const platform of [
PlatformType.wx,
PlatformType.bdzn,
PlatformType.dd,
PlatformType.jd,
PlatformType.qq,
PlatformType.zfb,
PlatformType.zj,
]) {
it(`运行${PlatformType[platform]}`, async () => {
angularConfig.platform = platform;
const root = harness.host.root();
const myTestProjectHost = new MyTestProjectHost(harness.host);
const list = await myTestProjectHost.getFileList(
normalize(join(root, 'src', '__pages'))
);
list.push(
...(await myTestProjectHost.getFileList(
normalize(join(root, 'src', '__components'))
))
);
await myTestProjectHost.importPathRename(list);
await myTestProjectHost.moveDir(ALL_PAGE_NAME_LIST, '__pages', 'pages');
await myTestProjectHost.moveDir(
ALL_COMPONENT_NAME_LIST,
'__components',
'components'
);
await myTestProjectHost.addPageEntry(ALL_PAGE_NAME_LIST);
harness.useTarget('build', angularConfig);
const result = await harness.executeOnce();
expect(result).toBeTruthy();
expect(result.error).toBeFalsy();
expect(result.logs[0].level !== 'error').toBeTruthy();
expect(result.result?.success).toBeTruthy();
const injectList = getBuildPlatformInjectConfig(angularConfig.platform);
const injector = Injector.create({ providers: injectList });
const buildPlatform = injector.get(BuildPlatform);
harness
.expectFile(
join(
normalize(DEFAULT_ANGULAR_CONFIG.outputPath),
`app${buildPlatform.fileExtname.style}`
)
)
.toExist();
const libraryPath = join(
normalize(DEFAULT_ANGULAR_CONFIG.outputPath),
LIBRARY_OUTPUT_ROOTDIR,
'test-library'
);
const librarySelfTemplateFile = harness.expectFile(
join(libraryPath, `self${buildPlatform.fileExtname.contentTemplate}`)
);
librarySelfTemplateFile.toExist();
librarySelfTemplateFile.content.toContain(`$$mp$$__self__$$`);
TEST_LIBRARY_COMPONENT_LIST.forEach((item) => {
const componentPath = join(libraryPath, item, item);
harness
.expectFile(componentPath + buildPlatform.fileExtname.logic)
.toExist();
harness
.expectFile(
componentPath + (buildPlatform.fileExtname.config || '.json')
)
.toExist();
harness
.expectFile(componentPath + buildPlatform.fileExtname.content)
.toExist();
});
const realTestPath: string = result.result?.baseOutputPath as string;
const appTestPath = path.resolve(process.cwd(), '__test-app');
fs.copySync(realTestPath, path.resolve(process.cwd(), '__test-app'));
// ('等待断点放开');
fs.removeSync(appTestPath);
});
}
});
});
================================================
FILE: src/builder/builder.watch.spec.ts
================================================
import { join, normalize } from '@angular-devkit/core';
import fs from 'node:fs';
import path from 'node:path';
import { of } from 'rxjs';
import { concatMap, skip, take } from 'rxjs/operators';
import {
MyTestProjectHost,
describeBuilder,
setWorkspaceRoot,
} from '../../test/plugin-describe-builder';
import {
BROWSER_BUILDER_INFO,
DEFAULT_ANGULAR_CONFIG,
} from '../../test/test-builder';
import {
ALL_COMPONENT_NAME_LIST,
ALL_PAGE_NAME_LIST,
} from '../../test/util/file';
import { runBuilder } from './application';
import { PlatformType } from './platform/platform';
const angularConfig = {
...DEFAULT_ANGULAR_CONFIG,
platform: PlatformType.wx,
watch: true,
};
describeBuilder(
runBuilder,
{ ...BROWSER_BUILDER_INFO, name: 'test-builder:watch' },
(harness) => {
describe('builder-watch-dev', () => {
it('运行', async () => {
const root = harness.host.root();
const myTestProjectHost = new MyTestProjectHost(harness.host);
const list = await myTestProjectHost.getFileList(
normalize(join(root, 'src', '__pages'))
);
list.push(
...(await myTestProjectHost.getFileList(
normalize(join(root, 'src', '__components'))
))
);
await myTestProjectHost.importPathRename(list);
await myTestProjectHost.moveDir(ALL_PAGE_NAME_LIST, '__pages', 'pages');
await myTestProjectHost.moveDir(
ALL_COMPONENT_NAME_LIST,
'__components',
'components'
);
await myTestProjectHost.addPageEntry(ALL_PAGE_NAME_LIST);
let finish: Function;
const waitFinish = new Promise((res) => {
finish = res;
});
harness.useTarget('build', angularConfig);
harness
.execute()
.pipe(
concatMap((result, index) => {
if (index) {
return of(result);
}
const value = JSON.parse(harness.readFile('src/app.json'));
value.pages.push(`pages/sub3/sub3-entry`);
const data = readFixture('watch/sub3', 'src/pages/sub3');
harness
.writeFiles({
'src/app.json': JSON.stringify(value),
...data,
})
.then(
(res) => {},
(rej) => {
throw rej;
}
);
return of(result);
}),
take(2),
skip(1)
)
.subscribe((result) => {
expect(result.logs[0].level !== 'error').toBeTruthy();
expect(result).toBeTruthy();
expect(result.error).toBeFalsy();
expect(result.result?.success).toBeTruthy();
expect(result.logs[0].message).toContain('sub3-entry.js');
harness
.expectFile(
join(
normalize(DEFAULT_ANGULAR_CONFIG.outputPath),
'pages/sub3/sub3-entry.js'
)
)
.toExist();
harness
.expectFile(
join(
normalize(DEFAULT_ANGULAR_CONFIG.outputPath),
'pages/sub3/sub3-entry.json'
)
)
.toExist();
harness
.expectFile(
join(
normalize(DEFAULT_ANGULAR_CONFIG.outputPath),
'pages/sub3/sub3-entry.wxml'
)
)
.toExist();
harness
.expectFile(
join(
normalize(DEFAULT_ANGULAR_CONFIG.outputPath),
'pages/sub3/sub3-entry.wxss'
)
)
.toExist();
harness
.expectFile(
join(
normalize(DEFAULT_ANGULAR_CONFIG.outputPath),
'library/test-library/lib-comp1-component/lib-comp1-component.js'
)
)
.toExist();
finish();
});
await waitFinish;
});
});
}
);
function readFixture(dir: string, to: string) {
const dirPath = path.resolve(__dirname, 'test/fixture', dir);
const list = fs.readdirSync(dirPath);
const fileObject: Record = {};
for (const item of list) {
const filePath = path.resolve(dirPath, item);
const content = fs.readFileSync(filePath, { encoding: 'utf8' });
fileObject[`${path.posix.join(to, item)}`] = content;
}
return fileObject;
}
================================================
FILE: src/builder/builders.json
================================================
{
"$schema": "../../node_modules/@angular-devkit/architect/src/builders-schema.json",
"builders": {
"application": {
"implementation": "./application",
"schema": "./application/schema.json",
"description": "小程序构建builder"
},
"library": {
"implementation": "./library/builder",
"schema": "./library/schema.json",
"description": "小程序构建library"
},
"karma": {
"implementation": "./karma",
"schema": "./karma/schema.json",
"description": "小程序测试"
}
}
}
================================================
FILE: src/builder/component-template-inject/change-component.ts
================================================
import {
Change,
InsertChange,
TsChange,
createCssSelectorForTs,
} from 'cyia-code-util';
import * as ts from 'typescript';
import { RawUpdater } from '../util';
export function changeComponent(data: string) {
const sf = ts.createSourceFile('', data, ts.ScriptTarget.Latest, true);
const selector = createCssSelectorForTs(sf);
const ɵcmpNodeList = selector.queryAll(
`PropertyAccessExpression[name=ɵɵdefineComponent]~SyntaxList ObjectLiteralExpression`
) as ts.BinaryExpression[];
if (!ɵcmpNodeList.length) {
return undefined;
}
const changeList: Change[] = [];
for (const componentNode of ɵcmpNodeList) {
const ɵcmpNode = componentNode;
const templateNode = selector.queryOne(
ɵcmpNode,
`PropertyAssignment[name=template]::initializer`
) as ts.PropertyAssignment;
const initIfNode = selector.queryOne(
templateNode,
`IfStatement[expression="rf & 1"]`
) as ts.IfStatement;
if (!initIfNode) {
continue;
}
const change = new TsChange(sf);
let updateInsertChange: InsertChange;
changeList.push(
new InsertChange(0, `import * as amp from 'angular-miniprogram';\n`)
);
changeList.push(
new InsertChange(0, `import * as ampNgCore from '@angular/core';\n`)
);
const updateIfNode = selector.queryOne(
templateNode,
`IfStatement[expression="rf & 2"]`
) as ts.IfStatement;
const updateContent = `amp.propertyChange(ampNgCore.ɵɵgetCurrentView());`;
if (updateIfNode) {
const updateBlock = updateIfNode.thenStatement as ts.Block;
updateInsertChange = change.insertNode(
updateBlock.statements[updateBlock.statements.length - 1],
`;${updateContent}`,
'end'
);
} else {
updateInsertChange = change.insertNode(
initIfNode,
`if(rf & 2){${updateContent}}`,
'end'
);
}
changeList.push(updateInsertChange);
}
return {
content: RawUpdater.update(data, changeList),
// todo library可否支持同文件多组件
componentName: selector
.queryOne(ɵcmpNodeList[0], 'PropertyAssignment[name=type]::initializer')
.getText(),
};
}
================================================
FILE: src/builder/karma/client/adapter.ts
================================================
import { KarmaClient } from './karma';
// Save link to native Date object
// before it might be mocked by the user
const _Date = Date;
/**
* Decision maker for whether a stack entry is considered external to jasmine and karma.
* @param {String} entry Error stack entry.
* @return {Boolean} True if external, False otherwise.
*/
function isExternalStackEntry(entry: string) {
return (
!!entry &&
// entries related to jasmine and karma-jasmine:
!/\/(jasmine-core|karma-jasmine)\//.test(entry) &&
// karma specifics, e.g. "at http://localhost:7018/karma.js:185"
!/\/(karma.js|context.html):/.test(entry)
);
}
/**
* Returns relevant stack entries.
* @param {Array} stack frames
* @return {Array} A list of relevant stack entries.
*/
function getRelevantStackFrom(stack: string[]): string[] {
let filteredStack: string[] = [];
const relevantStack: string[] = [];
for (let i = 0; i < stack.length; i += 1) {
if (isExternalStackEntry(stack[i])) {
filteredStack.push(stack[i]);
}
}
// If the filtered stack is empty, i.e. the error originated entirely from within jasmine or karma, then the whole stack
// should be relevant.
if (filteredStack.length === 0) {
filteredStack = stack;
}
for (let i = 0; i < filteredStack.length; i += 1) {
if (filteredStack[i]) {
relevantStack.push(filteredStack[i]);
}
}
return relevantStack;
}
/**
* Custom formatter for a failed step.
*
* Different browsers report stack trace in different ways. This function
* attempts to provide a concise, relevant error message by removing the
* unnecessary stack traces coming from the testing framework itself as well
* as possible repetition.
*
* @see https://github.com/karma-runner/karma-jasmine/issues/60
* @param {Object} step Step object with stack and message properties.
* @return {String} Formatted step.
*/
function formatFailedStep(step: Record): string {
const relevantMessage: string[] = [];
const relevantStack: string[] = [];
// Safari/Firefox seems to have no stack trace,
// so we just return the error message and if available
// construct a stacktrace out of filename and lineno:
if (!step.stack) {
if (step.filename) {
let stackframe: string = step.filename;
if (step.lineno) {
stackframe = stackframe + ':' + step.lineno;
}
relevantStack.push(stackframe);
}
relevantMessage.push(step.message);
return relevantMessage.concat(relevantStack).join('\n');
}
// Remove the message prior to processing the stack to prevent issues like
// https://github.com/karma-runner/karma-jasmine/issues/79
const stackframes = step.stack.split('\n');
let messageOnStack = null;
if (stackframes[0].indexOf(step.message) !== -1) {
// Remove the message if it is in the stack string (eg Chrome)
messageOnStack = stackframes.shift();
}
// Filter frames
const relevantStackFrames = getRelevantStackFrom(stackframes);
if (messageOnStack) {
// Put the message back if we removed it.
relevantStackFrames.unshift(messageOnStack);
} else {
// The stack did not have the step.message so add it.
relevantStackFrames.unshift(step.message);
}
return relevantStackFrames.join('\n');
}
class SuiteNode {
constructor(public name?: string, public parent?: SuiteNode) {}
description!: string;
children: any[] = [];
addChild(name: string) {
const suite = new SuiteNode(name, this);
this.children.push(suite);
return suite;
}
}
function processSuite(suite: SuiteNode, pointer: Record) {
let child;
let childPointer;
for (let i = 0; i < suite.children.length; i++) {
child = suite.children[i];
if (child.children) {
childPointer = pointer[child.description] = { _: [] };
processSuite(child, childPointer);
} else {
if (!pointer._) {
pointer._ = [];
}
pointer._.push(child.description);
}
}
}
function getAllSpecNames(topSuite: SuiteNode) {
const specNames = {};
processSuite(topSuite, specNames);
return specNames;
}
/**
* Very simple reporter for Jasmine.
*/
class KarmaReporter implements jasmine.CustomReporter {
currentSuite = new SuiteNode();
startTimeCurrentSpec = new _Date().getTime();
constructor(private tc: KarmaClient, private jasmineEnv: jasmine.Env) {}
handleGlobalErrors(result: Record) {
if (result.failedExpectations && result.failedExpectations.length) {
let message: string = 'An error was thrown in afterAll';
const steps = result.failedExpectations;
for (let i = 0, l = steps.length; i < l; i++) {
message += '\n' + formatFailedStep(steps[i]);
}
this.tc.error(message);
}
}
/**
* Jasmine 2.0 dispatches the following events:
*
* - jasmineStarted
* - jasmineDone
* - suiteStarted
* - suiteDone
* - specStarted
* - specDone
*/
jasmineStarted(data: Record) {
// TODO(vojta): Do not send spec names when polling.
this.tc.info({
event: 'jasmineStarted',
total: data.totalSpecsDefined,
specs: getAllSpecNames(this.jasmineEnv.topSuite() as any),
});
}
jasmineDone(result: Record) {
result = result || {};
// Any errors in top-level afterAll blocks are given here.
this.handleGlobalErrors(result);
// Remove functions from called back results to avoid IPC errors in Electron
// https://github.com/twolfson/karma-electron/issues/47
let cleanedOrder!: Record;
if (result.order) {
cleanedOrder = {};
const orderKeys = Object.getOwnPropertyNames(result.order);
for (let i = 0; i < orderKeys.length; i++) {
const orderKey = orderKeys[i];
if (typeof result.order[orderKey] !== 'function') {
cleanedOrder[orderKey] = result.order[orderKey];
}
}
}
// todo 单元测试覆盖率移除
this.tc.complete({
order: cleanedOrder,
coverage: undefined,
});
}
suiteStarted(result: Record) {
this.currentSuite = this.currentSuite.addChild(result.description);
this.tc.info({
event: 'suiteStarted',
result: result,
});
}
suiteDone(result: Record) {
// In the case of xdescribe, only "suiteDone" is fired.
// We need to skip that.
if (result.description !== this.currentSuite.name) {
return;
}
// Any errors in afterAll blocks are given here, except for top-level
// afterAll blocks.
this.handleGlobalErrors(result);
this.currentSuite = this.currentSuite.parent!;
this.tc.info({
event: 'suiteDone',
result: result,
});
}
specStarted() {
this.startTimeCurrentSpec = new _Date().getTime();
}
specDone(specResult: Record) {
const skipped =
specResult.status === 'disabled' ||
specResult.status === 'pending' ||
specResult.status === 'excluded';
const result = {
fullName: specResult.fullName,
description: specResult.description,
id: specResult.id,
log: [] as string[],
skipped: skipped,
disabled:
specResult.status === 'disabled' || specResult.status === 'excluded',
pending: specResult.status === 'pending',
success: specResult.failedExpectations.length === 0,
suite: [] as string[],
time: skipped ? 0 : new _Date().getTime() - this.startTimeCurrentSpec,
executedExpectationsCount:
specResult.failedExpectations.length +
specResult.passedExpectations.length,
passedExpectations: specResult.passedExpectations,
properties: specResult.properties,
};
// generate ordered list of (nested) suite names
let suitePointer = this.currentSuite;
while (suitePointer.parent) {
result.suite.unshift(suitePointer.name!);
suitePointer = suitePointer.parent;
}
if (!result.success) {
const steps = specResult.failedExpectations;
for (let i = 0, l = steps.length; i < l; i++) {
result.log.push(formatFailedStep(steps[i]));
}
// todo 永远不可能赋值当前
// Report the name of fhe failing spec so the reporter can emit a debug url.
// (result as any).debug_url = debugUrl(specResult.fullName);
}
// When failSpecWithNoExpectations is true, Jasmine will report specs without expectations as failed
if (
result.executedExpectationsCount === 0 &&
specResult.status === 'failed'
) {
result.success = false;
result.log.push('Spec has no expectations');
}
this.tc.result(result);
delete specResult.startTime;
}
}
/**
* Extract grep option from karma config
* @param {[Array|string]} clientArguments The karma client arguments
* @return {string} The value of grep option by default empty string
*/
const getGrepOption = function (clientArguments: any[] | string) {
const grepRegex = /^--grep=(.*)$/;
if (Object.prototype.toString.call(clientArguments) === '[object Array]') {
const indexOfGrep = indexOf(clientArguments as any[], '--grep');
if (indexOfGrep !== -1) {
return clientArguments[indexOfGrep + 1];
}
return (
map(
filter(clientArguments as any[], function (arg: string) {
return grepRegex.test(arg);
}),
function (arg) {
return arg.replace(grepRegex, '$1');
}
)[0] || ''
);
} else if (typeof clientArguments === 'string') {
const match = /--grep=([^=]+)/.exec(clientArguments);
return match ? match[1] : '';
}
};
const createRegExp = function (filter: string) {
filter = filter || '';
if (filter === '') {
return new RegExp(''); // to match all
}
const regExp = /^[/](.*)[/]([gmixXsuUAJD]*)$/; // pattern to check whether the string is RegExp pattern
const parts = regExp.exec(filter);
if (parts === null) {
return new RegExp(filter.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&')); // escape functional symbols
}
const patternExpression = parts[1];
const patternSwitches = parts[2];
return new RegExp(patternExpression, patternSwitches);
};
function getGrepSpecsToRun(clientConfig: Record, specs: any[]) {
const grepOption = getGrepOption(clientConfig.args);
if (grepOption) {
const regExp = createRegExp(grepOption);
return filter(specs, function specFilter(spec: any) {
return regExp.test(spec.getFullName());
});
}
}
function parseQueryParams(location: Record) {
const params: Record = {};
if (location && Object.prototype.hasOwnProperty.call(location, 'search')) {
const pairs = location.search.substr(1).split('&');
for (let i = 0; i < pairs.length; i++) {
const keyValue = pairs[i].split('=');
params[decodeURIComponent(keyValue[0])] = decodeURIComponent(keyValue[1]);
}
}
return params;
}
function getId(s: { id: any }) {
return s.id;
}
function getSpecsByName(specs: any[], name: string) {
specs = specs.filter(function (s) {
return s.name.indexOf(name) !== -1;
});
if (specs.length === 0) {
throw new Error('No spec found with name: "' + name + '"');
}
return specs;
}
function getDebugSpecToRun(location: Record, specs: any[]) {
const queryParams: Record = parseQueryParams(location);
const spec = queryParams.spec;
if (spec) {
// A single spec has been requested by name for debugging.
return getSpecsByName(specs, spec);
}
}
function getSpecsToRunForCurrentShard(
specs: any[],
shardIndex: number,
totalShards: number
) {
if (specs.length < totalShards) {
throw new Error(
'More shards (' + totalShards + ') than test specs (' + specs.length + ')'
);
}
// Just do a simple sharding strategy of dividing the number of specs
// equally.
const firstSpec = Math.floor((specs.length * shardIndex) / totalShards);
const lastSpec = Math.floor((specs.length * (shardIndex + 1)) / totalShards);
return specs.slice(firstSpec, lastSpec);
}
function getShardedSpecsToRun(specs: any[], clientConfig: Record) {
const shardIndex = clientConfig.shardIndex;
const totalShards = clientConfig.totalShards;
if (shardIndex != null && totalShards != null) {
// Sharded mode - Run only the subset of the specs corresponding to the
// current shard.
return getSpecsToRunForCurrentShard(
specs,
Number(shardIndex),
Number(totalShards)
);
}
}
/**
* Create jasmine spec filter
* @param {Object} clientConfig karma config
* @param {!Object} jasmineEnv
*/
class KarmaSpecFilter {
specIdsToRun: any[];
constructor(
private clientConfig: Record,
private jasmineEnv: jasmine.Env
) {
this.specIdsToRun = this.getSpecsToRun(
undefined as any,
this.clientConfig,
this.jasmineEnv
).map(getId);
}
/**
* Walk the test suite tree depth first and collect all test specs
* @param {!Object} jasmineEnv
* @return {!Array} All possible tests.
*/
getAllSpecs(jasmineEnv: jasmine.Env) {
const specs: (jasmine.Suite | jasmine.Spec)[] = [];
let stack = [jasmineEnv.topSuite()];
let currentNode: jasmine.Suite;
while ((currentNode = stack.pop()!)) {
if (currentNode.children) {
// jasmine.Suite
stack = stack.concat(currentNode.children as jasmine.Suite[]);
} else if (currentNode.id) {
// jasmine.Spec
specs.unshift(currentNode);
}
}
return specs;
}
/**
* Filter the specs with URL search params and config.
* @param {!Object} location property 'search' from URL.
* @param {!Object} clientConfig karma client config
* @param {!Object} jasmineEnv
* @return {!Array}
*/
getSpecsToRun(
location: Record,
clientConfig: Record,
jasmineEnv: jasmine.Env
) {
const specs = this.getAllSpecs(jasmineEnv).map(function (spec) {
(spec as any).name = spec.getFullName();
return spec;
});
if (!specs || !specs.length) {
return [];
}
return (
getGrepSpecsToRun(clientConfig, specs) ||
getDebugSpecToRun(location, specs) ||
getShardedSpecsToRun(specs, clientConfig) ||
specs
);
}
matches(spec: jasmine.Suite) {
return this.specIdsToRun.indexOf(spec.id) !== -1;
}
}
/**
* Configure jasmine specFilter
*
* This function is invoked from the wrapper.
* @see adapter.wrapper
*
* @param {Object} config The karma config
* @param {Object} jasmineEnv jasmine environment object
*/
const createSpecFilter = function (
config: Record,
jasmineEnv: jasmine.Env
) {
const karmaSpecFilter = new KarmaSpecFilter(config, jasmineEnv);
const specFilter = function (spec: jasmine.Suite) {
return karmaSpecFilter.matches(spec);
};
return specFilter;
};
/**
* Karma starter function factory.
*
* This function is invoked from the wrapper.
* @see adapter.wrapper
*
* @param {Object} karma Karma runner instance.
* @param {Object} [jasmineEnv] Optional Jasmine environment for testing.
* @return {Function} Karma starter function.
*/
export function createStartFn(karma: KarmaClient, jasmineEnv: jasmine.Env) {
// This function will be assigned to `window.__karma__.start`:
return function () {
const clientConfig = {
args: [],
useIframe: true,
runInParent: false,
captureConsole: true,
clearContext: false,
jasmine: {},
originalArgs: [],
};
const jasmineConfig = clientConfig.jasmine || {};
jasmineEnv = jasmineEnv || jasmine.getEnv();
(jasmineConfig as any).specFilter = createSpecFilter(
clientConfig,
jasmineEnv
);
jasmineEnv.configure(jasmineConfig);
jasmine.DEFAULT_TIMEOUT_INTERVAL =
(jasmineConfig as any).timeoutInterval ||
jasmine.DEFAULT_TIMEOUT_INTERVAL;
jasmineEnv.addReporter(new KarmaReporter(karma, jasmineEnv));
jasmineEnv.execute();
};
}
function indexOf(collection: any[], find: any, i?: number /* opt */) {
if (collection.indexOf) {
return collection.indexOf(find, i);
}
if (i === undefined) {
i = 0;
}
if (i < 0) {
i += collection.length;
}
if (i < 0) {
i = 0;
}
for (let n = collection.length; i < n; i++) {
if (i in collection && collection[i] === find) {
return i;
}
}
return -1;
}
function filter(collection: any[], filter: any, that?: any /* opt */) {
if (collection.filter) {
return collection.filter(filter, that);
}
const other: any[] = [];
let v: any;
for (let i = 0, n = collection.length; i < n; i++) {
if (
i in collection &&
filter.call(that, (v = collection[i]), i, collection)
) {
other.push(v);
}
}
return other;
}
function map(
collection: any[],
mapper: (value: any, index: number, array: any[]) => any,
that?: any /* opt */
) {
if (collection.map) {
return collection.map(mapper, that);
}
const other = new Array(collection.length);
for (let i = 0, n = collection.length; i < n; i++) {
if (i in collection) {
other[i] = mapper.call(that, collection[i], i, collection);
}
}
return other;
}
================================================
FILE: src/builder/karma/client/index.ts
================================================
export * from './main';
================================================
FILE: src/builder/karma/client/karma.ts
================================================
import { IO } from './platform';
import { StatusUpdater } from './updater';
export class KarmaClient {
/** 是否正式发射判断? */
private startEmitted = false;
public config: Record = {};
/** socket重连接标记 */
private socketReconnect = false;
private resultsBufferLimit = 50;
private resultsBuffer: any[] = [];
private returnUrl!: string;
readonly id: string = 'miniprogram';
constructor(private updater: StatusUpdater, private socket: IO) {
socket.on('execute', (cfg) => {
this.updater.updateTestStatus('execute');
// reset startEmitted and reload the iframe
this.startEmitted = false;
this.config = cfg;
});
socket.on('stop', () => {
this.complete();
});
// 初始化的时候自动有这个.
socket.on('connect', () => {
socket.emit('register', {
name: '小程序',
id: this.id,
isSocketReconnect: this.socketReconnect,
});
this.socketReconnect = true;
});
}
private navigateContextTo(url: string) {}
log(type: string, args: any[]) {
const values: any[] = [];
for (let i = 0; i < args.length; i++) {
values.push(JSON.stringify(args[i]));
}
this.info({ log: values.join(', '), type: type });
}
private getLocation(url?: string, lineno?: string, colno?: string) {
let location = '';
if (url !== undefined) {
location += url;
}
if (lineno !== undefined) {
location += ':' + lineno;
}
if (colno !== undefined) {
location += ':' + colno;
}
return location;
}
error(
messageOrEvent: string | Error,
source?: string,
lineno?: string,
colno?: string,
error?: Error
) {
let message: string | Record;
if (typeof messageOrEvent === 'string') {
message = messageOrEvent;
const location = this.getLocation(source, lineno, colno);
if (location !== '') {
message += '\nat ' + location;
}
if (error && error.stack) {
message += '\n\n' + error.stack;
}
} else {
// create an object with the string representation of the message to
// ensure all its content is properly transferred to the console log
message = { message: messageOrEvent, str: messageOrEvent.toString() };
}
this.socket.emit('karma_error', message);
this.updater.updateTestStatus('karma_error ' + message);
this.complete();
return false;
}
result(originalResult: Record) {
const convertedResult: Record = {};
// Convert all array-like objects to real arrays.
for (const propertyName in originalResult) {
if (Object.prototype.hasOwnProperty.call(originalResult, propertyName)) {
const propertyValue = originalResult[propertyName];
if (
Object.prototype.toString.call(propertyValue) === '[object Array]'
) {
convertedResult[propertyName] =
Array.prototype.slice.call(propertyValue);
} else {
convertedResult[propertyName] = propertyValue;
}
}
}
if (!this.startEmitted) {
this.socket.emit('start', { total: null });
this.updater.updateTestStatus('start');
this.startEmitted = true;
}
if (this.resultsBufferLimit === 1) {
this.updater.updateTestStatus('result');
return this.socket.emit('result', convertedResult);
}
this.resultsBuffer.push(convertedResult);
if (this.resultsBuffer.length === this.resultsBufferLimit) {
this.socket.emit('result', this.resultsBuffer);
this.updater.updateTestStatus('result');
this.resultsBuffer = [];
}
}
complete(result?: Record) {
if (this.resultsBuffer.length) {
this.socket.emit('result', this.resultsBuffer);
this.resultsBuffer = [];
}
this.socket.emit('complete', result || {});
if (this.config.clearContext) {
this.navigateContextTo('about:blank');
} else {
this.updater.updateTestStatus('complete');
}
if (this.returnUrl) {
let isReturnUrlAllowed = false;
for (let i = 0; i < this.config.allowedReturnUrlPatterns.length; i++) {
const allowedReturnUrlPattern = new RegExp(
this.config.allowedReturnUrlPatterns[i]
);
if (allowedReturnUrlPattern.test(this.returnUrl)) {
isReturnUrlAllowed = true;
break;
}
}
if (!isReturnUrlAllowed) {
throw new Error(
'Security: Navigation to '.concat(
this.returnUrl,
' was blocked to prevent malicious exploits.'
)
);
}
}
}
/** 可以直接使用 */
info(info: any) {
// TODO(vojta): introduce special API for this
if (!this.startEmitted && info.total) {
this.socket.emit('start', info);
this.startEmitted = true;
} else {
this.socket.emit('info', info);
}
}
}
================================================
FILE: src/builder/karma/client/main.ts
================================================
import { createStartFn } from './adapter';
import { KarmaClient } from './karma';
import { IO } from './platform';
import { StatusUpdater } from './updater';
declare const KARMA_CLIENT_CONFIG: any;
export function startupTest() {
const socket = new IO();
const updater = new StatusUpdater(socket);
const karmaClient = new KarmaClient(updater, socket);
if (KARMA_CLIENT_CONFIG.captureConsole) {
// patch the console
const localConsole = console || {
log: function () {},
info: function () {},
warn: function () {},
error: function () {},
debug: function () {},
};
const logMethods: (keyof Console)[] = [
'log',
'info',
'warn',
'error',
'debug',
];
const patchConsoleMethod = function (method: keyof Console) {
const orig = localConsole[method];
if (!orig) {
return;
}
localConsole[method] = function () {
try {
return Function.prototype.apply.call(orig, localConsole, arguments);
} catch (error) {
karmaClient.log('warn', [
'Console method ' + method + ' threw: ' + error,
]);
}
karmaClient.log(method, Array.from(arguments));
} as any;
};
for (let i = 0; i < logMethods.length; i++) {
patchConsoleMethod(logMethods[i]);
}
}
createStartFn(karmaClient, jasmine.getEnv())();
}
================================================
FILE: src/builder/karma/client/platform/index.ts
================================================
export * from './wx';
================================================
FILE: src/builder/karma/client/platform/wx/index.ts
================================================
///
import type { Socket } from 'socket.io-client';
const io = require('weapp.socket.io/lib/weapp.socket.io');
declare const KARMA_PORT: number;
export class IO {
instance: Socket;
constructor() {
this.instance = io(`http://localhost:${KARMA_PORT}`);
}
on(data: string, callback: (...args: any[]) => void) {
this.instance.on(data, (...args) => {
callback(...args);
});
}
emit(type: string, data: any) {
this.instance.emit(type, data);
}
}
================================================
FILE: src/builder/karma/client/tsconfig.json
================================================
{
"compilerOptions": {
"outDir": "../../../../dist/karma/client",
"strict": false,
"strictNullChecks": false,
"allowUnreachableCode": true,
"noUnusedParameters": false,
"sourceMap": true,
"declaration": true,
"target": "ES2021",
"module": "CommonJS",
"lib": ["ES2021"],
"skipLibCheck": true
},
"files": ["index.ts"]
}
================================================
FILE: src/builder/karma/client/updater.ts
================================================
import { IO } from './platform';
export class StatusUpdater {
private connectionText = 'never-connected';
private testText = 'loading';
private pingText = '';
constructor(private socket: IO) {
socket.on('connect', () => {
this.updateConnectionStatus('connected');
});
socket.on('disconnect', () => {
this.updateConnectionStatus('disconnected');
});
socket.on('reconnecting', (sec) => {
this.updateConnectionStatus('reconnecting in ' + sec + ' seconds');
});
socket.on('reconnect', () => {
this.updateConnectionStatus('reconnected');
});
socket.on('reconnect_failed', () => {
this.updateConnectionStatus('reconnect_failed');
});
socket.on('info', () => this.updateBrowsersInfo([]));
socket.on('disconnect', () => {
this.updateBrowsersInfo([]);
});
socket.on('ping', () => {
this.updatePingStatus('ping...');
});
socket.on('pong', (latency) => {
this.updatePingStatus('ping ' + latency + 'ms');
});
}
private updateBrowsersInfo(browsers: any[]) {}
private updateBanner() {}
private updateConnectionStatus(connectionStatus: string) {
this.connectionText = connectionStatus || this.connectionText;
this.updateBanner();
}
updateTestStatus(testStatus: string) {
this.testText = testStatus || this.testText;
this.updateBanner();
}
private updatePingStatus(pingStatus: string) {
this.pingText = pingStatus || this.pingText;
this.updateBanner();
}
}
================================================
FILE: src/builder/karma/index.origin.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
BuilderContext,
BuilderOutput,
createBuilder,
} from '@angular-devkit/architect';
import {
BrowserBuilderOptions,
OutputHashing,
} from '@angular-devkit/build-angular';
import type {
ExecutionTransformer,
KarmaBuilderOptions,
} from '@angular-devkit/build-angular';
import { FindTestsPlugin } from '@angular-devkit/build-angular/src/builders/karma/find-tests-plugin';
import {
getCommonConfig,
getStylesConfig,
} from '@angular-devkit/build-angular/src/tools/webpack/configs';
import { purgeStaleBuildCache } from '@angular-devkit/build-angular/src/utils/purge-cache';
import { assertCompatibleAngularVersion } from '@angular-devkit/build-angular/src/utils/version';
import { generateBrowserWebpackConfigFromContext } from '@angular-devkit/build-angular/src/utils/webpack-browser-config';
import { Config, ConfigOptions } from 'karma';
import * as path from 'path';
import { dirname, resolve } from 'path';
import { Observable, from } from 'rxjs';
import { defaultIfEmpty, switchMap } from 'rxjs/operators';
import type { Configuration } from 'webpack';
export type KarmaConfigOptions = ConfigOptions & {
buildWebpack?: unknown;
configFile?: string;
};
async function initialize(
options: KarmaBuilderOptions,
context: BuilderContext,
webpackConfigurationTransformer?: ExecutionTransformer
): Promise<[typeof import('karma'), Configuration]> {
// Purge old build disk cache.
await purgeStaleBuildCache(context);
const { config } = await generateBrowserWebpackConfigFromContext(
// only two properties are missing:
// * `outputPath` which is fixed for tests
// * `budgets` which might be incorrect due to extra dev libs
{
...(options as unknown as BrowserBuilderOptions),
outputPath: '',
budgets: undefined,
optimization: false,
buildOptimizer: false,
aot: true,
vendorChunk: true,
namedChunks: true,
extractLicenses: false,
outputHashing: OutputHashing.None,
// The webpack tier owns the watch behavior so we want to force it in the config.
// When not in watch mode, webpack-dev-middleware will call `compiler.watch` anyway.
// https://github.com/webpack/webpack-dev-middleware/blob/698c9ae5e9bb9a013985add6189ff21c1a1ec185/src/index.js#L65
// https://github.com/webpack/webpack/blob/cde1b73e12eb8a77eb9ba42e7920c9ec5d29c2c9/lib/Compiler.js#L379-L388
watch: true,
},
context,
(wco) => [
getCommonConfig(wco),
// getBrowserConfig(wco),
getStylesConfig(wco),
// getTypeScriptConfig(wco),
]
);
const karma = await import('karma');
return [
karma,
webpackConfigurationTransformer
? await webpackConfigurationTransformer(config)
: config,
];
}
/**
* @experimental Direct usage of this function is considered experimental.
*/
export function execute(
options: KarmaBuilderOptions,
context: BuilderContext,
transforms: {
webpackConfiguration?: ExecutionTransformer;
// The karma options transform cannot be async without a refactor of the builder implementation
karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions;
} = {}
): Observable {
// Check Angular version.
assertCompatibleAngularVersion(context.workspaceRoot);
let singleRun: boolean | undefined;
if (options.watch !== undefined) {
singleRun = !options.watch;
}
return from(
initialize(options, context, transforms.webpackConfiguration)
).pipe(
switchMap(async ([karma, webpackConfig]) => {
const karmaOptions: KarmaConfigOptions = {
singleRun,
};
// Convert browsers from a string to an array
if (options.browsers) {
karmaOptions.browsers = (options.browsers as string)!.split(',');
}
if (options.reporters) {
// Split along commas to make it more natural, and remove empty strings.
const reporters = options.reporters
.reduce((acc, curr) => acc.concat(curr.split(',')), [])
.filter((x) => !!x);
if (reporters.length > 0) {
karmaOptions.reporters = reporters;
}
}
const projectName = context.target?.project;
if (!projectName) {
throw new Error('The builder requires a target.');
}
const projectMetadata = await context.getProjectMetadata(projectName);
const sourceRoot = (projectMetadata.sourceRoot ??
projectMetadata.root ??
'') as string;
webpackConfig.plugins ??= [];
webpackConfig.plugins.push(
new FindTestsPlugin({
// include: options.include,
workspaceRoot: context.workspaceRoot,
projectSourceRoot: path.join(context.workspaceRoot, sourceRoot),
})
);
karmaOptions.buildWebpack = {
options,
webpackConfig,
logger: context.logger,
};
const config = await karma.config.parseConfig(
resolve(context.workspaceRoot, options.karmaConfig!),
transforms.karmaOptions
? transforms.karmaOptions(karmaOptions)
: karmaOptions,
{ promiseConfig: true, throwErrors: true }
);
return [karma, config] as [typeof karma, KarmaConfigOptions];
}),
switchMap(
([karma, karmaConfig]) =>
new Observable((subscriber) => {
// Pass onto Karma to emit BuildEvents.
karmaConfig.buildWebpack ??= {};
if (typeof karmaConfig.buildWebpack === 'object') {
(karmaConfig.buildWebpack as any).failureCb ??= () =>
subscriber.next({ success: false });
(karmaConfig.buildWebpack as any).successCb ??= () =>
subscriber.next({ success: true });
(karmaConfig.buildWebpack as any).testContext = (
context as any
).testContext;
}
// Complete the observable once the Karma server returns.
const karmaServer = new karma.Server(
karmaConfig as Config,
(exitCode) => {
subscriber.next({ success: exitCode === 0 });
subscriber.complete();
}
);
const karmaStart = karmaServer.start();
// Cleanup, signal Karma to exit.
return () => karmaStart.then(() => karmaServer.stop());
})
),
defaultIfEmpty({ success: false })
);
}
export { KarmaBuilderOptions };
export default createBuilder & KarmaBuilderOptions>(
execute
);
================================================
FILE: src/builder/karma/index.spec.ts
================================================
import { BuilderOutput } from '@angular-devkit/architect';
import { join, normalize } from '@angular-devkit/core';
import * as fs from 'fs-extra';
import * as path from 'path';
import {
BuilderHarnessExecutionResult,
MyTestProjectHost,
describeBuilder,
} from '../../../test/plugin-describe-builder';
import {
DEFAULT_ANGULAR_KARMA_CONFIG,
KARMA_BUILDER_INFO,
} from '../../../test/test-builder';
import { runBuilder } from './index';
const angularConfig = {
...DEFAULT_ANGULAR_KARMA_CONFIG,
watch: true,
};
describeBuilder(runBuilder, KARMA_BUILDER_INFO, (harness) => {
// 此测试仅能本地使用,并且只能一个测试用例单独开启
xdescribe('karma', () => {
it('运行', async () => {
const root = harness.host.root();
const myTestProjectHost = new MyTestProjectHost(harness.host);
await myTestProjectHost.addSpecEntry([
'empty',
'tag-view-convert-spec',
'style-class-spec',
'life-time-spec',
'ng-if-spec',
'http-spec',
'ng-content-spec',
'ng-for-spec',
'ng-library-import-spec',
'ng-switch-spec',
'ng-template-outlet-spec',
'self-template-spec',
]);
harness.useTarget('build', angularConfig);
let appTestPath: string;
const result = new Promise>(
(res) => {
let result;
harness
.execute({
testContext: {
buildSuccess: async (webpackConfig) => {
const realTestPath: string = webpackConfig.output!
.path as string;
appTestPath = path.resolve(process.cwd(), '__test-app');
fs.removeSync(appTestPath);
fs.copySync(realTestPath, appTestPath);
},
},
})
.subscribe({
next: (value) => (result = value),
complete: () => res(result),
});
}
);
await result;
expect((await result).error).toBe(undefined);
expect((await result).result.success).toBe(true);
fs.removeSync(appTestPath);
});
xit('watch', async () => {
const root = harness.host.root();
const myTestProjectHost = new MyTestProjectHost(harness.host);
await myTestProjectHost.addSpecEntry([
'empty',
'tag-view-convert-spec',
'style-class-spec',
'life-time-spec',
'ng-if-spec',
'http-spec',
'ng-content-spec',
'ng-for-spec',
'ng-library-import-spec',
'ng-switch-spec',
'ng-template-outlet-spec',
'self-template-spec',
]);
harness.useTarget('build', { ...angularConfig, watch: true });
let appTestPath: string;
const result = new Promise>(
(res) => {
let first = true;
harness
.execute({
testContext: {
buildSuccess: async (webpackConfig) => {
const realTestPath: string = webpackConfig.output!
.path as string;
appTestPath = path.resolve(process.cwd(), '__test-app');
if (!first) {
fs.removeSync(appTestPath);
fs.copySync(realTestPath, appTestPath);
return;
}
first = false;
fs.removeSync(appTestPath);
fs.copySync(realTestPath, appTestPath);
setTimeout(async () => {
await writeFile();
}, 0);
},
},
})
.subscribe({
next: (value) => res(value),
});
}
);
await result;
expect((await result).error).toBe(undefined);
expect((await result).result.success).toBe(true);
fs.removeSync(appTestPath);
});
});
function writeFile() {
const fileContent = harness.readFile('src/spec/ng-if-spec/ng-if.spec.ts');
return harness.writeFiles({
'src/spec/ng-if-spec/ng-if.spec.ts': `${fileContent}
describe('test-add',()=>{it('main',()=>{expect(true).toBe(true)})});`,
});
}
});
================================================
FILE: src/builder/karma/index.ts
================================================
import { BuilderContext, createBuilder } from '@angular-devkit/architect';
import {
AssetPattern,
KarmaBuilderOptions,
} from '@angular-devkit/build-angular';
import { Injector } from 'static-injector';
import * as webpack from 'webpack';
import { WebpackConfigurationChangeService } from '../application/webpack-configuration-change.service';
import {
BuildPlatform,
PlatformType,
getBuildPlatformInjectConfig,
} from '../platform';
import { execute } from './index.origin';
export default createBuilder(
(
angularOptions: KarmaBuilderOptions & {
pages: AssetPattern[];
components: AssetPattern[];
platform: PlatformType;
},
context: BuilderContext
): ReturnType => {
return runBuilder(angularOptions, context);
}
);
export function runBuilder(
angularOptions: KarmaBuilderOptions & {
pages: AssetPattern[];
components: AssetPattern[];
platform: PlatformType;
},
context: BuilderContext
): ReturnType {
return execute(angularOptions, context, {
webpackConfiguration: async (options: webpack.Configuration) => {
const injector = Injector.create({
providers: [
...getBuildPlatformInjectConfig(PlatformType.wx),
{
provide: WebpackConfigurationChangeService,
useFactory: (injector: Injector) => {
return new WebpackConfigurationChangeService(
angularOptions,
context,
options,
injector
);
},
deps: [Injector],
},
],
});
const config = injector.get(WebpackConfigurationChangeService);
config.init();
await config.change();
const buildPlatform = injector.get(BuildPlatform);
options.plugins!.push(
new webpack.DefinePlugin({
describe: `${buildPlatform.globalVariablePrefix}.describe`,
xdescribe: `${buildPlatform.globalVariablePrefix}.xdescribe`,
fdescribe: `${buildPlatform.globalVariablePrefix}.fdescribe`,
it: `${buildPlatform.globalVariablePrefix}.it`,
xit: `${buildPlatform.globalVariablePrefix}.xit`,
fit: `${buildPlatform.globalVariablePrefix}.fit`,
beforeEach: `${buildPlatform.globalVariablePrefix}.beforeEach`,
afterEach: `${buildPlatform.globalVariablePrefix}.afterEach`,
beforeAll: `${buildPlatform.globalVariablePrefix}.beforeAll`,
afterAll: `${buildPlatform.globalVariablePrefix}.afterAll`,
setSpecProperty: `${buildPlatform.globalVariablePrefix}.setSpecProperty`,
setSuiteProperty: `${buildPlatform.globalVariablePrefix}.setSuiteProperty`,
expect: `${buildPlatform.globalVariablePrefix}.expect`,
expectAsync: `${buildPlatform.globalVariablePrefix}.expectAsync`,
pending: `${buildPlatform.globalVariablePrefix}.pending`,
fail: `${buildPlatform.globalVariablePrefix}.fail`,
spyOn: `${buildPlatform.globalVariablePrefix}.spyOn`,
spyOnProperty: `${buildPlatform.globalVariablePrefix}.spyOnProperty`,
spyOnAllFunctions: `${buildPlatform.globalVariablePrefix}.spyOnAllFunctions`,
jsApiReporter: `${buildPlatform.globalVariablePrefix}.jsApiReporter`,
jasmine: `${buildPlatform.globalVariablePrefix}.jasmine`,
})
);
options.output!.path += '/dist';
return options;
},
});
}
================================================
FILE: src/builder/karma/plugin/index.js
================================================
require('ts-node').register({
/* options */
scope: true,
cwd: __dirname,
});
let obj = require('./launcher');
obj = { ...obj.default, ...require('./karma').default };
module.exports = obj;
================================================
FILE: src/builder/karma/plugin/index.ts
================================================
import config from './karma';
import launcher from './launcher';
module.exports = { ...config, ...launcher };
================================================
FILE: src/builder/karma/plugin/karma.ts
================================================
// import { statsErrorsToString } from '@angular-devkit/build-angular/src/webpack/utils/stats';
import { logging } from '@angular-devkit/core';
import { createConsoleLogger } from '@angular-devkit/core/node';
import { ConfigOptions, launcher } from 'karma';
import * as webpack from 'webpack';
launcher.Launcher.generateId = () => {
return 'miniprogram';
};
let blocked: any[] = [];
let isBlocked = false;
let successCb: () => void;
let failureCb: () => void;
function init(
config: ConfigOptions & {
buildWebpack: {
logger: logging.Logger;
failureCb: () => void;
successCb: () => void;
testContext: { buildSuccess: (arg: webpack.Configuration) => void };
webpackConfig: webpack.Configuration;
};
configFile?: string;
webpack?: webpack.Configuration;
},
emitter: any
) {
if (!config.buildWebpack) {
throw new Error(
`The '@angular-devkit/build-angular/plugins/karma' karma plugin is meant to` +
` be used from within Angular CLI and will not work correctly outside of it.`
);
}
// const options = config.buildWebpack.options as BuildOptions;
const logger: logging.Logger =
config.buildWebpack.logger || createConsoleLogger();
successCb = config.buildWebpack.successCb;
failureCb = config.buildWebpack.failureCb;
config.reporters?.unshift('@angular-devkit/build-angular--event-reporter');
// todo 可能用不上,因为时本地
// When using code-coverage, auto-add karma-coverage.
// if (
// options!.codeCoverage &&
// !config.reporters.some((r: string) => r === 'coverage' || r === 'coverage-istanbul')
// ) {
// config.reporters.push('coverage');
// }
// Add webpack config.
const webpackConfig = config.buildWebpack
.webpackConfig as webpack.Configuration;
// Use existing config if any.
config.webpack = { ...webpackConfig, ...config.webpack };
// Our custom context and debug files list the webpack bundles directly instead of using
// the karma files array.
if (config.singleRun) {
// There's no option to turn off file watching in webpack-dev-server, but
// we can override the file watcher instead.
(webpackConfig.plugins as any[]).unshift({
apply: (compiler: any) => {
compiler.hooks.afterEnvironment.tap('karma', () => {
compiler.watchFileSystem = { watch: () => {} };
});
},
});
}
webpackConfig.plugins!.push(
new webpack.DefinePlugin({
KARMA_CLIENT_CONFIG: JSON.stringify(config.client),
KARMA_PORT: config.port,
})
);
// Files need to be served from a custom path for Karma.
const compiler = webpack.webpack(webpackConfig, (error, stats) => {
if (error) {
throw error;
}
if (stats?.hasErrors()) {
// Only generate needed JSON stats and when needed.
const statsJson = stats?.toJson({
all: false,
children: true,
errors: true,
warnings: true,
});
logger.error(JSON.stringify(statsJson));
// Notify potential listeners of the compile error.
emitter.emit('compile_error', {
errors: statsJson.errors?.map((e) => e.message),
});
// Finish Karma run early in case of compilation error.
emitter.emit('run_complete', [], { exitCode: 1 });
// Emit a failure build event if there are compilation errors.
failureCb();
return;
}
// 仅测试时使用
if (config.buildWebpack.testContext) {
config.buildWebpack.testContext.buildSuccess(webpackConfig);
}
});
function handler(callback?: () => void): void {
isBlocked = true;
callback?.();
}
compiler.hooks.invalid.tap('karma', () => handler(() => {}));
compiler.hooks.watchRun.tapAsync('karma', (_: any, callback: () => void) =>
handler(callback)
);
compiler.hooks.run.tapAsync('karma', (_: any, callback: () => void) =>
handler(callback)
);
function unblock() {
isBlocked = false;
blocked.forEach((cb) => cb());
blocked = [];
}
let lastCompilationHash: string | undefined;
compiler.hooks.done.tap('karma', (stats) => {
if (stats.hasErrors()) {
lastCompilationHash = undefined;
} else if (stats.hash != lastCompilationHash) {
// Refresh karma only when there are no webpack errors, and if the compilation changed.
lastCompilationHash = stats.hash;
emitter.refreshFiles();
}
unblock();
});
emitter.on('exit', (done: any) => {
done();
});
}
init.$inject = ['config', 'emitter'];
// Block requests until the Webpack compilation is done.
function requestBlocker() {
return function (_request: any, _response: any, next: () => void) {
if (isBlocked) {
blocked.push(next);
} else {
next();
}
};
}
// Copied from "karma-jasmine-diff-reporter" source code:
// In case, when multiple reporters are used in conjunction
// with initSourcemapReporter, they both will show repetitive log
// messages when displaying everything that supposed to write to terminal.
// So just suppress any logs from initSourcemapReporter by doing nothing on
// browser log, because it is an utility reporter,
// unless it's alone in the "reporters" option and base reporter is used.
function muteDuplicateReporterLogging(context: any, config: any) {
context.writeCommonMsg = () => {};
const reporterName = '@angular/cli';
const hasTrailingReporters =
config.reporters.slice(-1).pop() !== reporterName;
if (hasTrailingReporters) {
context.writeCommonMsg = () => {};
}
}
// Emits builder events.
const eventReporter: any = function (
this: any,
baseReporterDecorator: any,
config: any
) {
baseReporterDecorator(this);
muteDuplicateReporterLogging(this, config);
this.onRunComplete = function (_browsers: any, results: any) {
if (results.exitCode === 0) {
successCb();
} else {
failureCb();
}
};
// avoid duplicate failure message
this.specFailure = () => {};
};
eventReporter.$inject = ['baseReporterDecorator', 'config'];
// When a request is not found in the karma server, try looking for it from the webpack server root.
export default {
'framework:@angular-devkit/build-angular': ['factory', init],
'reporter:@angular-devkit/build-angular--event-reporter': [
'type',
eventReporter,
],
};
================================================
FILE: src/builder/karma/plugin/launcher.ts
================================================
const miniProgram = function (
this: any,
baseBrowserDecorator: any,
config: any
) {
baseBrowserDecorator(this);
const self = this;
this.name = 'miniprogram';
this._start = function (url: string) {};
this.on('kill', function (done: any) {
self.emit('done');
process.nextTick(done);
});
};
miniProgram.$inject = ['baseBrowserDecorator', 'config.jsdomLauncher'];
export default {
'launcher:miniprogram': ['type', miniProgram],
};
================================================
FILE: src/builder/karma/plugin/tsconfig.json
================================================
{
"compilerOptions": {
"outDir": "../../../../dist/karma/plugin",
"strict": false,
"strictNullChecks": false,
"allowUnreachableCode": true,
"noUnusedParameters": false,
"sourceMap": true,
"declaration": true,
"esModuleInterop": true,
"target": "ES2021",
"module": "CommonJS",
"lib": [
"ES2021"
],
"skipLibCheck": true
},
"files": [
"./index.ts"
]
}
================================================
FILE: src/builder/karma/schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "Karma Target",
"description": "Karma target options for Build Facade.",
"type": "object",
"properties": {
"pages": {
"type": "array",
"description": "页面配置",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"components": {
"type": "array",
"description": "组件配置",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"platform": {
"type": "string",
"description": "小程序平台",
"default": "wx"
},
"main": {
"type": "string",
"description": "The name of the main entry-point file."
},
"tsConfig": {
"type": "string",
"description": "The name of the TypeScript configuration file."
},
"karmaConfig": {
"type": "string",
"description": "The name of the Karma configuration file."
},
"polyfills": {
"type": "string",
"description": "The name of the polyfills file."
},
"assets": {
"type": "array",
"description": "List of static application assets.",
"default": [],
"items": {
"$ref": "#/definitions/assetPattern"
}
},
"scripts": {
"description": "Global scripts to be included in the build.",
"type": "array",
"default": [],
"items": {
"$ref": "#/definitions/extraEntryPoint"
}
},
"styles": {
"description": "Global styles to be included in the build.",
"type": "array",
"default": [],
"items": {
"$ref": "#/definitions/extraEntryPoint"
}
},
"inlineStyleLanguage": {
"description": "The stylesheet language to use for the application's inline component styles.",
"type": "string",
"default": "css",
"enum": ["css", "less", "sass", "scss"]
},
"stylePreprocessorOptions": {
"description": "Options to pass to style preprocessors",
"type": "object",
"properties": {
"includePaths": {
"description": "Paths to include. Paths will be resolved to workspace root.",
"type": "array",
"items": {
"type": "string"
},
"default": []
}
},
"additionalProperties": false
},
"include": {
"type": "array",
"items": {
"type": "string"
},
"description": "Globs of files to include, relative to workspace or project root. \nThere are 2 special cases:\n - when a path to directory is provided, all spec files ending \".spec.@(ts|tsx)\" will be included\n - when a path to a file is provided, and a matching spec file exists it will be included instead"
},
"sourceMap": {
"description": "Output source maps for scripts and styles. For more information, see https://angular.io/guide/workspace-config#source-map-configuration.",
"default": true,
"oneOf": [
{
"type": "object",
"properties": {
"scripts": {
"type": "boolean",
"description": "Output source maps for all scripts.",
"default": true
},
"styles": {
"type": "boolean",
"description": "Output source maps for all styles.",
"default": true
},
"vendor": {
"type": "boolean",
"description": "Resolve vendor packages source maps.",
"default": false
}
},
"additionalProperties": false
},
{
"type": "boolean"
}
]
},
"progress": {
"type": "boolean",
"description": "Log progress to the console while building.",
"default": true
},
"watch": {
"type": "boolean",
"description": "Run build when files change."
},
"poll": {
"type": "number",
"description": "Enable and define the file watching poll time period in milliseconds."
},
"preserveSymlinks": {
"type": "boolean",
"description": "Do not use the real path when resolving modules. If unset then will default to `true` if NodeJS option --preserve-symlinks is set."
},
"browsers": {
"type": "string",
"description": "Override which browsers tests are run against."
},
"codeCoverage": {
"type": "boolean",
"description": "Output a code coverage report.",
"default": false
},
"codeCoverageExclude": {
"type": "array",
"description": "Globs to exclude from code coverage.",
"items": {
"type": "string"
},
"default": []
},
"fileReplacements": {
"description": "Replace compilation source files with other compilation source files in the build.",
"type": "array",
"items": {
"oneOf": [
{
"type": "object",
"properties": {
"src": {
"type": "string"
},
"replaceWith": {
"type": "string"
}
},
"additionalProperties": false,
"required": ["src", "replaceWith"]
},
{
"type": "object",
"properties": {
"replace": {
"type": "string"
},
"with": {
"type": "string"
}
},
"additionalProperties": false,
"required": ["replace", "with"]
}
]
},
"default": []
},
"reporters": {
"type": "array",
"description": "Karma reporters to use. Directly passed to the karma runner.",
"items": {
"type": "string"
}
},
"webWorkerTsConfig": {
"type": "string",
"description": "TypeScript configuration for Web Worker modules."
}
},
"additionalProperties": false,
"required": ["main", "tsConfig", "karmaConfig"],
"definitions": {
"assetPattern": {
"oneOf": [
{
"type": "object",
"properties": {
"glob": {
"type": "string",
"description": "The pattern to match."
},
"input": {
"type": "string",
"description": "The input directory path in which to apply 'glob'. Defaults to the project root."
},
"output": {
"type": "string",
"description": "Absolute path within the output."
},
"ignore": {
"description": "An array of globs to ignore.",
"type": "array",
"items": {
"type": "string"
}
}
},
"additionalProperties": false,
"required": ["glob", "input", "output"]
},
{
"type": "string"
}
]
},
"extraEntryPoint": {
"oneOf": [
{
"type": "object",
"properties": {
"input": {
"type": "string",
"description": "The file to include."
},
"bundleName": {
"type": "string",
"pattern": "^[\\w\\-.]*$",
"description": "The bundle name for this extra entry point."
},
"inject": {
"type": "boolean",
"description": "If the bundle will be referenced in the HTML file.",
"default": true
}
},
"additionalProperties": false,
"required": ["input"]
},
{
"type": "string",
"description": "The file to include."
}
]
}
}
}
================================================
FILE: src/builder/library/add-declaration-metadata.service.ts
================================================
import type {
R3ComponentMetadata,
R3DirectiveMetadata,
} from '@angular/compiler';
import { createCssSelectorForTs } from 'cyia-code-util';
import { Inject, Injectable } from 'static-injector';
import ts from 'typescript';
import { MiniProgramCompilerService } from '../mini-program-compiler';
import {
LIBRARY_COMPONENT_OUTPUT_PATH_SUFFIX,
LIBRARY_DIRECTIVE_LISTENERS_SUFFIX,
LIBRARY_DIRECTIVE_PROPERTIES_SUFFIX,
} from './const';
import { getComponentOutputPath } from './get-library-path';
import { ENTRY_POINT_TOKEN } from './token';
@Injectable()
export class AddDeclarationMetaDataService {
private directiveMap: Map;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private componentMap: Map>;
constructor(
@Inject(ENTRY_POINT_TOKEN) private entryPoint: string,
miniProgramCompilerService: MiniProgramCompilerService
) {
this.directiveMap = miniProgramCompilerService.getDirectiveMap();
this.componentMap = miniProgramCompilerService.getComponentMap();
}
run(dTsFileName: string, data: string): string {
const list = createCssSelectorForTs(data).queryAll(
`ClassDeclaration`
) as ts.ClassDeclaration[];
return (
data +
this.addComponentMetaDataDeclaration(list) +
this.addDirectiveMetaDataDeclaration(list)
);
}
private addComponentMetaDataDeclaration(list: ts.ClassDeclaration[]) {
const metaList = ['\n'];
for (let i = 0; i < list.length; i++) {
const classDeclaration = list[i];
const isComponentClassDeclaration = classDeclaration.members.some(
(item) =>
ts.isPropertyDeclaration(item) &&
item.modifiers?.some((modifier) => modifier.getText() === 'static') &&
item.name.getText() === 'ɵcmp'
);
if (!isComponentClassDeclaration) {
continue;
}
metaList.push(
...this.getPropertyAndListener(classDeclaration, this.componentMap)
);
const className = classDeclaration.name!.getText();
metaList.push(
`declare const ${className}_${LIBRARY_COMPONENT_OUTPUT_PATH_SUFFIX}:"${getComponentOutputPath(
this.entryPoint,
className
)}";`
);
}
return metaList.join('\n');
}
private addDirectiveMetaDataDeclaration(list: ts.ClassDeclaration[]) {
const metaList = ['\n'];
for (let i = 0; i < list.length; i++) {
const classDeclaration = list[i];
const isDirectiveClassDeclaration = classDeclaration.members.some(
(item) =>
ts.isPropertyDeclaration(item) &&
item.modifiers?.some((modifier) => modifier.getText() === 'static') &&
item.name.getText() === 'ɵdir'
);
if (!isDirectiveClassDeclaration) {
continue;
}
metaList.push(
...this.getPropertyAndListener(classDeclaration, this.directiveMap)
);
}
return metaList.join('\n');
}
private getPropertyAndListener(
classDeclaration: ts.ClassDeclaration,
map: Map
) {
const className: string = classDeclaration.name!.getText();
const list: string[] = [];
for (const [key, meta] of map.entries()) {
const directiveClassName = meta.name;
if (directiveClassName === className) {
const listeners = meta.host.listeners as Record;
list.push(
`declare const ${className}_${LIBRARY_DIRECTIVE_LISTENERS_SUFFIX}:${JSON.stringify(
Object.keys(listeners)
)};`
);
const properties = meta.host.properties as Record;
list.push(
`declare const ${className}_${LIBRARY_DIRECTIVE_PROPERTIES_SUFFIX}:${JSON.stringify(
Object.keys(properties)
)};`
);
break;
}
}
return list;
}
}
================================================
FILE: src/builder/library/builder.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
BuilderContext,
BuilderOutput,
createBuilder,
} from '@angular-devkit/architect';
import { normalizeCacheOptions } from '@angular-devkit/build-angular/src/utils/normalize-cache';
import { join, resolve } from 'path';
import { Observable, from, of } from 'rxjs';
import { catchError, mapTo, switchMap } from 'rxjs/operators';
import { ngPackagrFactory } from './ng-packagr-factory';
/**
* @experimental Direct usage of this function is considered experimental.
*/
export function execute(
options: any,
context: BuilderContext
): Observable {
return from(
(async () => {
const root = context.workspaceRoot;
let tsConfig: string | undefined;
if (options.tsConfig) {
tsConfig = resolve(root, options.tsConfig);
}
const packager = await ngPackagrFactory(
resolve(root, options.project),
tsConfig
);
const projectName = context.target?.project;
if (!projectName) {
throw new Error('The builder requires a target.');
}
const metadata = await context.getProjectMetadata(projectName);
const { enabled: cacheEnabled, path: cacheDirectory } =
normalizeCacheOptions(metadata, context.workspaceRoot);
const ngPackagrOptions = {
cacheEnabled,
cacheDirectory: join(cacheDirectory, 'ng-packagr'),
};
return { packager, ngPackagrOptions };
})()
).pipe(
switchMap(({ packager, ngPackagrOptions }) =>
options.watch
? packager.watch(ngPackagrOptions)
: packager.build(ngPackagrOptions)
),
mapTo({ success: true, workspaceRoot: context.workspaceRoot }),
catchError((err) => of({ success: false, error: err.message }))
);
}
export default createBuilder & any>(execute);
================================================
FILE: src/builder/library/compile-ngc.transform.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
Transform,
transformFromPromise,
} from 'ng-packagr/lib/graph/transform';
import {
EntryPointNode,
PackageNode,
isEntryPoint,
isEntryPointInProgress,
isPackage,
} from 'ng-packagr/lib/ng-package/nodes';
import { NgPackagrOptions } from 'ng-packagr/lib/ng-package/options.di';
import { StylesheetProcessor as StylesheetProcessorClass } from 'ng-packagr/lib/styles/stylesheet-processor';
import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig';
import ora from 'ora';
import * as path from 'path';
import ts from 'typescript';
import { compileSourceFiles } from './compile-source-files';
export const myCompileNgcTransformFactory = (
StylesheetProcessor: typeof StylesheetProcessorClass,
options: NgPackagrOptions
): Transform => {
return transformFromPromise(async (graph) => {
const spinner = ora({
hideCursor: false,
discardStdin: false,
});
const entryPoints: EntryPointNode[] = graph.filter(isEntryPoint);
const entryPoint: EntryPointNode = entryPoints.find(
isEntryPointInProgress()
)!;
const ngPackageNode: PackageNode = graph.find(isPackage)!;
const projectBasePath = ngPackageNode.data.primary.basePath;
try {
// Add paths mappings for dependencies
const tsConfig = setDependenciesTsConfigPaths(
entryPoint.data.tsConfig!,
entryPoints
);
// Compile TypeScript sources
const { esm2022: esm2022, declarations } =
entryPoint.data.destinationFiles;
const { basePath, cssUrl, styleIncludePaths } =
entryPoint.data.entryPoint;
const { moduleResolutionCache } = entryPoint.cache;
spinner.start(
`Compiling with Angular sources in Ivy ${
tsConfig.options.compilationMode || 'full'
} compilation mode.`
);
entryPoint.cache.stylesheetProcessor ??= new StylesheetProcessor(
projectBasePath,
basePath,
cssUrl,
styleIncludePaths,
options.cacheEnabled && options.cacheDirectory
);
await compileSourceFiles(
graph,
tsConfig,
moduleResolutionCache,
{
outDir: path.dirname(esm2022),
declarationDir: path.dirname(declarations),
declaration: true,
target: ts.ScriptTarget.ES2022,
},
entryPoint.cache.stylesheetProcessor,
options.watch
);
} catch (error) {
spinner.fail();
throw error;
} finally {
if (!options.watch) {
entryPoint.cache.stylesheetProcessor?.destroy();
}
}
spinner.succeed();
return graph;
});
};
================================================
FILE: src/builder/library/compile-source-files.ts
================================================
import type {
CompilerOptions,
ParsedConfiguration,
} from '@angular/compiler-cli';
import { dirname, normalize } from '@angular-devkit/core';
import { BuildGraph } from 'ng-packagr/lib/graph/build-graph';
import {
EntryPointNode,
PackageNode,
isEntryPointInProgress,
isPackage,
} from 'ng-packagr/lib/ng-package/nodes';
import { StylesheetProcessor } from 'ng-packagr/lib/styles/stylesheet-processor';
import {
augmentProgramWithVersioning,
cacheCompilerHost,
} from 'ng-packagr/lib/ts/cache-compiler-host';
import { ngCompilerCli } from 'ng-packagr/lib/utils/load-esm';
import * as log from 'ng-packagr/lib/utils/log';
import { join } from 'node:path';
import path from 'path';
import { Injector } from 'static-injector';
import ts from 'typescript';
import { MiniProgramCompilerService } from '../mini-program-compiler';
import { BuildPlatform, PlatformType } from '../platform/platform';
import { getBuildPlatformInjectConfig } from '../platform/platform-inject-config';
import { AddDeclarationMetaDataService } from './add-declaration-metadata.service';
import { OutputTemplateMetadataService } from './output-template-metadata.service';
import { SetupComponentDataService } from './setup-component-data.service';
import { CustomStyleSheetProcessor } from './stylesheet-processor';
import {
ENTRY_FILE_TOKEN,
ENTRY_POINT_TOKEN,
RESOLVED_DATA_GROUP_TOKEN,
} from './token';
export async function compileSourceFiles(
graph: BuildGraph,
tsConfig: ParsedConfiguration,
moduleResolutionCache: ts.ModuleResolutionCache,
extraOptions?: Partial,
stylesheetProcessor?: StylesheetProcessor,
watch?: boolean
) {
const { NgtscProgram, formatDiagnostics } = await ngCompilerCli();
const tsConfigOptions: CompilerOptions = {
...tsConfig.options,
...extraOptions,
};
const entryPoint: EntryPointNode = graph.find(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isEntryPointInProgress() as any
)!;
const ngPackageNode: PackageNode = graph.find(isPackage)!;
const inlineStyleLanguage = ngPackageNode.data.inlineStyleLanguage;
const tsCompilerHost = cacheCompilerHost(
graph,
entryPoint,
tsConfigOptions,
moduleResolutionCache,
stylesheetProcessor,
inlineStyleLanguage
);
// inject
augmentLibraryMetadata(tsCompilerHost);
const cache = entryPoint.cache;
const sourceFileCache = cache.sourcesFileCache;
// Create the Angular specific program that contains the Angular compiler
const angularProgram = new NgtscProgram(
tsConfig.rootNames,
tsConfigOptions,
tsCompilerHost,
cache.oldNgtscProgram
);
const angularCompiler = angularProgram.compiler;
const { ignoreForDiagnostics, ignoreForEmit } = angularCompiler;
// SourceFile versions are required for builder programs.
// The wrapped host inside NgtscProgram adds additional files that will not have versions.
const typeScriptProgram = angularProgram.getTsProgram();
augmentProgramWithVersioning(typeScriptProgram);
let builder: ts.BuilderProgram | ts.EmitAndSemanticDiagnosticsBuilderProgram;
if (watch) {
builder = cache.oldBuilder =
ts.createEmitAndSemanticDiagnosticsBuilderProgram(
typeScriptProgram,
tsCompilerHost,
cache.oldBuilder
);
cache.oldNgtscProgram = angularProgram;
} else {
// When not in watch mode, the startup cost of the incremental analysis can be avoided by
// using an abstract builder that only wraps a TypeScript program.
builder = ts.createAbstractBuilder(typeScriptProgram, tsCompilerHost);
}
// Update semantic diagnostics cache
const affectedFiles = new Set();
// Analyze affected files when in watch mode for incremental type checking
if ('getSemanticDiagnosticsOfNextAffectedFile' in builder) {
// eslint-disable-next-line no-constant-condition
while (true) {
const result = builder.getSemanticDiagnosticsOfNextAffectedFile(
undefined,
(sourceFile) => {
// If the affected file is a TTC shim, add the shim's original source file.
// This ensures that changes that affect TTC are typechecked even when the changes
// are otherwise unrelated from a TS perspective and do not result in Ivy codegen changes.
// For example, changing @Input property types of a directive used in another component's
// template.
if (
ignoreForDiagnostics.has(sourceFile) &&
sourceFile.fileName.endsWith('.ngtypecheck.ts')
) {
// This file name conversion relies on internal compiler logic and should be converted
// to an official method when available. 15 is length of `.ngtypecheck.ts`
const originalFilename = sourceFile.fileName.slice(0, -15) + '.ts';
const originalSourceFile = builder.getSourceFile(originalFilename);
if (originalSourceFile) {
affectedFiles.add(originalSourceFile);
}
return true;
}
return false;
}
);
if (!result) {
break;
}
affectedFiles.add(result.affected as ts.SourceFile);
}
}
// Collect program level diagnostics
const allDiagnostics: ts.Diagnostic[] = [
...angularCompiler.getOptionDiagnostics(),
...builder.getOptionsDiagnostics(),
...builder.getGlobalDiagnostics(),
];
// inject
let injector = Injector.create({
providers: [
...getBuildPlatformInjectConfig(PlatformType.library),
{
provide: MiniProgramCompilerService,
useFactory: (injector: Injector, buildPlatform: BuildPlatform) => {
return new MiniProgramCompilerService(
angularProgram,
injector,
buildPlatform
);
},
deps: [Injector, BuildPlatform],
},
{
provide: ENTRY_FILE_TOKEN,
useValue: join(
dirname(normalize(tsConfig.rootNames[0])),
normalize(tsConfigOptions.flatModuleOutFile!)
),
},
{
provide: ENTRY_POINT_TOKEN,
useValue: entryPoint.data.entryPoint.moduleId,
},
],
});
const miniProgramCompilerService = injector.get(MiniProgramCompilerService);
// Required to support asynchronous resource loading
// Must be done before creating transformers or getting template diagnostics
await angularCompiler.analyzeAsync();
// inject
miniProgramCompilerService.init();
const metaMap =
await miniProgramCompilerService.exportComponentBuildMetaMap();
injector = Injector.create({
parent: injector,
providers: [
{ provide: RESOLVED_DATA_GROUP_TOKEN, useValue: metaMap },
{ provide: AddDeclarationMetaDataService },
{ provide: OutputTemplateMetadataService },
{ provide: SetupComponentDataService },
],
});
// Collect source file specific diagnostics
for (const sourceFile of builder.getSourceFiles()) {
if (!ignoreForDiagnostics.has(sourceFile)) {
allDiagnostics.push(
...builder.getDeclarationDiagnostics(sourceFile),
...builder.getSyntacticDiagnostics(sourceFile),
...builder.getSemanticDiagnostics(sourceFile)
);
}
if (sourceFile.isDeclarationFile) {
continue;
}
// Collect sources that are required to be emitted
if (
!ignoreForEmit.has(sourceFile) &&
!angularCompiler.incrementalCompilation.safeToSkipEmit(sourceFile)
) {
// If required to emit, diagnostics may have also changed
if (!ignoreForDiagnostics.has(sourceFile)) {
affectedFiles.add(sourceFile);
}
} else if (
sourceFileCache &&
!affectedFiles.has(sourceFile) &&
!ignoreForDiagnostics.has(sourceFile)
) {
// Use cached Angular diagnostics for unchanged and unaffected files
const angularDiagnostics =
sourceFileCache.getAngularDiagnostics(sourceFile);
if (angularDiagnostics?.length) {
allDiagnostics.push(...angularDiagnostics);
}
}
}
// Collect new Angular diagnostics for files affected by changes
for (const affectedFile of affectedFiles) {
const angularDiagnostics = angularCompiler.getDiagnosticsForFile(
affectedFile,
/** OptimizeFor.WholeProgram */ 1
);
allDiagnostics.push(...angularDiagnostics);
sourceFileCache.updateAngularDiagnostics(affectedFile, angularDiagnostics);
}
const otherDiagnostics = [];
const errorDiagnostics = [];
for (const diagnostic of allDiagnostics) {
if (diagnostic.category === ts.DiagnosticCategory.Error) {
errorDiagnostics.push(diagnostic);
} else {
otherDiagnostics.push(diagnostic);
}
}
if (otherDiagnostics.length) {
log.msg(formatDiagnostics(errorDiagnostics));
}
if (errorDiagnostics.length) {
throw new Error(formatDiagnostics(errorDiagnostics));
}
const transformers = angularCompiler.prepareEmit().transformers;
for (const sourceFile of builder.getSourceFiles()) {
if (!ignoreForEmit.has(sourceFile)) {
builder.emit(sourceFile, undefined, undefined, undefined, transformers);
}
}
function augmentLibraryMetadata(compilerHost: ts.CompilerHost) {
const oldWriteFile = compilerHost.writeFile;
compilerHost.writeFile = function (
fileName: string,
data: string,
writeByteOrderMark,
onError,
sourceFiles
) {
const entryFileName = injector.get(ENTRY_FILE_TOKEN);
if (fileName.endsWith('.map')) {
return oldWriteFile.call(
this,
fileName,
data,
writeByteOrderMark,
onError,
sourceFiles
);
}
if (fileName.endsWith('.d.ts')) {
const service = injector.get(AddDeclarationMetaDataService);
const result = service.run(fileName, data);
return oldWriteFile.call(
this,
fileName,
result,
writeByteOrderMark,
onError,
sourceFiles
);
}
const sourceFile = sourceFiles && sourceFiles[0];
if (sourceFile) {
if (
normalize(entryFileName) ===
normalize(sourceFile.fileName.replace(/\.ts$/, '.js'))
) {
const service = injector.get(OutputTemplateMetadataService);
const result = service.run(fileName, data, sourceFiles![0]);
return oldWriteFile.call(
this,
fileName,
result,
writeByteOrderMark,
onError,
sourceFiles
);
}
const originFileName = path.normalize(sourceFile.fileName);
const setupComponentDataService = injector.get(
SetupComponentDataService
);
const result = setupComponentDataService.run(
data,
originFileName,
stylesheetProcessor! as CustomStyleSheetProcessor
);
return oldWriteFile.call(
this,
fileName,
result,
writeByteOrderMark,
onError,
sourceFiles
);
}
return oldWriteFile.call(
this,
fileName,
data,
writeByteOrderMark,
onError,
sourceFiles
);
};
}
}
================================================
FILE: src/builder/library/const.ts
================================================
export const LIBRARY_OUTPUT_ROOTDIR = 'library';
export const LIBRARY_DIRECTIVE_LISTENERS_SUFFIX = 'Listeners';
export const LIBRARY_DIRECTIVE_PROPERTIES_SUFFIX = 'Properties';
export const LIBRARY_COMPONENT_OUTPUT_PATH_SUFFIX = 'OutputPath';
export const LIBRARY_COMPONENT_METADATA_SUFFIX = 'ExtraData';
export const GLOBAL_TEMPLATE_SUFFIX = 'Global_Template';
================================================
FILE: src/builder/library/get-library-path.ts
================================================
import { join, normalize } from '@angular-devkit/core';
import { camelize, dasherize } from '@angular-devkit/core/src/utils/strings';
export function getComponentOutputPath(entry: string, className: string) {
return join(
normalize(entry),
dasherize(camelize(className)),
dasherize(camelize(className))
);
}
================================================
FILE: src/builder/library/index.ts
================================================
export * from './const';
export * from './type';
================================================
FILE: src/builder/library/library.spec.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { join, normalize } from '@angular-devkit/core';
import * as fs from 'fs-extra';
import path from 'path';
import { describeBuilder } from '../../../test/plugin-describe-builder';
import {
DEFAULT_ANGULAR_LIBRARY_CONFIG,
LIBRARY_BUILDER_INFO,
} from '../../../test/test-builder';
import { execute } from './builder';
import { LIBRARY_COMPONENT_METADATA_SUFFIX } from './const';
describeBuilder(execute, LIBRARY_BUILDER_INFO, (harness) => {
describe('test-library', () => {
it('运行', async () => {
harness.useTarget('library', DEFAULT_ANGULAR_LIBRARY_CONFIG);
const result = await harness.executeOnce();
expect(result).toBeTruthy();
expect(result.result).toBeTruthy();
expect(result.result.success).toBeTruthy();
if (!result.result.success) {
console.error(result.result.error);
}
const workspaceRoot: string = (result.result as any).workspaceRoot;
const outputPath = normalize(`dist/test-library`);
const output = path.join(workspaceRoot, outputPath);
const entryFile = harness.expectFile(
join(outputPath, 'esm2022', 'test-library.mjs')
);
entryFile.toExist();
entryFile.content.toContain(`$self_Global_Template`);
const globalSelfTemplate = harness.expectFile(
join(
outputPath,
'esm2022',
'global-self-template',
'global-self-template.component.mjs'
)
);
globalSelfTemplate.toExist();
globalSelfTemplate.content.toContain(
`GlobalSelfTemplateComponent_${LIBRARY_COMPONENT_METADATA_SUFFIX}`
);
fs.copySync(
output,
path.resolve(
process.cwd(),
'test',
'hello-world-app',
'node_modules',
'test-library'
)
);
});
});
});
================================================
FILE: src/builder/library/merge-using-component-path.ts
================================================
import { join, normalize, resolve } from '@angular-devkit/core';
import { UseComponent } from '../mini-program-compiler';
import { LIBRARY_OUTPUT_ROOTDIR } from './const';
import { getComponentOutputPath } from './get-library-path';
export function getUseComponents(
libraryPath: UseComponent[],
localPath: UseComponent[],
moduleId: string
) {
const list = [...libraryPath];
list.push(
...localPath.map((item) => {
item.path = getComponentOutputPath(moduleId, item.className);
return item;
})
);
return list.reduce((pre, cur) => {
pre[cur.selector] = resolve(
normalize('/'),
join(normalize(LIBRARY_OUTPUT_ROOTDIR), cur.path)
);
return pre;
}, {} as Record);
}
================================================
FILE: src/builder/library/ng-packagr-factory.ts
================================================
import { COMPILE_NGC_TRANSFORM } from 'ng-packagr/lib/ng-package/entry-point/compile-ngc.di';
import { STYLESHEET_PROCESSOR } from 'ng-packagr/lib/styles/stylesheet-processor.di';
import { myCompileNgcTransformFactory } from './compile-ngc.transform';
import { hookWritePackage } from './remove-publish-only';
import { CustomStyleSheetProcessor } from './stylesheet-processor';
export async function ngPackagrFactory(
project: string,
tsConfig: string | undefined
) {
const packager = (await import('ng-packagr')).ngPackagr();
packager.forProject(project);
if (tsConfig) {
packager.withTsConfig(tsConfig);
}
COMPILE_NGC_TRANSFORM.useFactory = myCompileNgcTransformFactory;
STYLESHEET_PROCESSOR.useFactory = () => CustomStyleSheetProcessor;
packager.withProviders([COMPILE_NGC_TRANSFORM, hookWritePackage()]);
return packager;
}
================================================
FILE: src/builder/library/output-template-metadata.service.ts
================================================
import { join, normalize, resolve } from '@angular-devkit/core';
import { Inject, Injectable } from 'static-injector';
import ts from 'typescript';
import { MetaCollection, ResolvedDataGroup } from '../mini-program-compiler';
import { GLOBAL_TEMPLATE_SUFFIX, LIBRARY_OUTPUT_ROOTDIR } from './const';
import { getUseComponents } from './merge-using-component-path';
import {
ENTRY_FILE_TOKEN,
ENTRY_POINT_TOKEN,
RESOLVED_DATA_GROUP_TOKEN,
} from './token';
import { ExtraTemplateData } from './type';
@Injectable()
export class OutputTemplateMetadataService {
private selfUseComponents!: Record;
private selfMetaCollection!: MetaCollection;
constructor(
@Inject(ENTRY_FILE_TOKEN) private entryFile: string,
@Inject(RESOLVED_DATA_GROUP_TOKEN)
private dataGroup: ResolvedDataGroup,
@Inject(ENTRY_POINT_TOKEN) private entryPoint: string
) {}
run(fileName: string, data: string, sourceFile: ts.SourceFile) {
const list = data.split(/\n|\r\n/g);
list.splice(
list.length - 1,
0,
`${this.getSelfTemplate()};${this.getLibraryTemplate()}`
);
return list.join('\n');
}
private getSelfTemplate() {
const selfMetaCollection = this.dataGroup.otherMetaCollectionGroup['$self'];
if (!selfMetaCollection) {
return '';
}
this.selfMetaCollection = selfMetaCollection;
const templateStr = selfMetaCollection.templateList
.map((item) => item.content)
.join('');
const extraTemplateData: ExtraTemplateData = {
template: templateStr,
outputPath: resolve(
normalize('/'),
join(normalize(LIBRARY_OUTPUT_ROOTDIR), this.entryPoint, 'self')
),
};
delete this.dataGroup.otherMetaCollectionGroup['$self'];
return `let $self_${GLOBAL_TEMPLATE_SUFFIX}=${JSON.stringify(
extraTemplateData
)}`;
}
private getLibraryTemplate() {
if (!Object.keys(this.dataGroup.otherMetaCollectionGroup).length) {
return '';
}
const obj: Record = {};
for (const key in this.dataGroup.otherMetaCollectionGroup) {
if (
Object.prototype.hasOwnProperty.call(
this.dataGroup.otherMetaCollectionGroup,
key
)
) {
const element = this.dataGroup.otherMetaCollectionGroup[key];
const templateStr = element.templateList
.map((item) => item.content)
.join('');
const useComponents = getUseComponents(
Array.from(element.libraryPath),
Array.from(element.localPath),
this.entryPoint
);
const extraTemplateData: ExtraTemplateData = {
template: templateStr,
useComponents: useComponents,
};
obj[key] = extraTemplateData;
}
}
return `let library_${GLOBAL_TEMPLATE_SUFFIX}=${JSON.stringify(obj)}`;
}
getSelfUseComponents() {
if (!this.selfUseComponents) {
const selfMetaCollection =
this.selfMetaCollection ||
this.dataGroup.otherMetaCollectionGroup['$self'];
if (!selfMetaCollection) {
return {};
}
const useComponents = getUseComponents(
Array.from(selfMetaCollection.libraryPath),
Array.from(selfMetaCollection.localPath),
this.entryPoint
);
this.selfUseComponents = useComponents;
}
return this.selfUseComponents;
}
}
================================================
FILE: src/builder/library/remove-publish-only.ts
================================================
import fs from 'fs-extra';
import { transformFromPromise } from 'ng-packagr/lib/graph/transform';
import { WRITE_PACKAGE_TRANSFORM } from 'ng-packagr/lib/ng-package/entry-point/write-package.di';
import {
EntryPointNode,
isEntryPointInProgress,
} from 'ng-packagr/lib/ng-package/nodes';
import { NgPackagrOptions } from 'ng-packagr/lib/ng-package/options.di';
import path from 'path';
import { of } from 'rxjs';
const oldFactory = WRITE_PACKAGE_TRANSFORM.useFactory;
export function hookWritePackage() {
WRITE_PACKAGE_TRANSFORM.useFactory = myWritePackage;
return WRITE_PACKAGE_TRANSFORM;
}
function myWritePackage(options: NgPackagrOptions) {
return transformFromPromise(async (graph) => {
// todo 这里理论上不应该这么做,因为rxjs非同依赖会有一些小问题.但是不涉及到unsubscribe遇不到这些小问题,所以先这么做,未来再想办法
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await oldFactory(options)(of(graph) as any).toPromise();
const entryPoint: EntryPointNode = graph.find(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
isEntryPointInProgress() as any
)!;
if (!entryPoint.data.entryPoint.isSecondaryEntryPoint) {
const packageJsonPath = path.resolve(
entryPoint.data.entryPoint.destinationPath,
'package.json'
);
const packageJson = JSON.parse(
fs.readFileSync(packageJsonPath).toString()
);
delete packageJson.scripts.prepublishOnly;
fs.writeFileSync(
packageJsonPath,
JSON.stringify(packageJson, undefined, 2)
);
}
});
}
================================================
FILE: src/builder/library/schema.json
================================================
{
"$schema": "http://json-schema.org/draft-07/schema",
"title": "ng-packagr Target",
"description": "ng-packagr target options for Build Architect. Use to build library projects.",
"type": "object",
"properties": {
"project": {
"type": "string",
"description": "The file path for the ng-packagr configuration file, relative to the current workspace."
},
"tsConfig": {
"type": "string",
"description": "The full path for the TypeScript configuration file, relative to the current workspace."
},
"watch": {
"type": "boolean",
"description": "Run build when files change.",
"default": false
}
},
"additionalProperties": false,
"required": ["project"]
}
================================================
FILE: src/builder/library/setup-component-data.service.ts
================================================
import { join, normalize, resolve, strings } from '@angular-devkit/core';
import { Inject, Injectable } from 'static-injector';
import { changeComponent } from '../component-template-inject/change-component';
import type { ExportLibraryComponentMeta } from '../library';
import { ResolvedDataGroup } from '../mini-program-compiler';
import { BuildPlatform } from '../platform/platform';
import {
LIBRARY_COMPONENT_METADATA_SUFFIX,
LIBRARY_OUTPUT_ROOTDIR,
} from './const';
import { getComponentOutputPath } from './get-library-path';
import { getUseComponents } from './merge-using-component-path';
import { OutputTemplateMetadataService } from './output-template-metadata.service';
import { CustomStyleSheetProcessor } from './stylesheet-processor';
import { ENTRY_POINT_TOKEN, RESOLVED_DATA_GROUP_TOKEN } from './token';
@Injectable()
export class SetupComponentDataService {
constructor(
@Inject(RESOLVED_DATA_GROUP_TOKEN)
private dataGroup: ResolvedDataGroup,
@Inject(ENTRY_POINT_TOKEN) private entryPoint: string,
private addGlobalTemplateService: OutputTemplateMetadataService,
private buildPlatform: BuildPlatform
) {}
run(
data: string,
originFileName: string,
customStyleSheetProcessor: CustomStyleSheetProcessor
) {
const changedData = changeComponent(data);
if (!changedData) {
return data;
}
const useComponentPath =
this.dataGroup.useComponentPath.get(originFileName)!;
const componentClassName = changedData.componentName;
const componentDirName = strings.dasherize(
strings.camelize(componentClassName)
);
const libraryPath = getComponentOutputPath(
this.entryPoint,
componentClassName
);
const styleUrlList = this.dataGroup.style.get(originFileName);
const styleContentList: string[] = [];
styleUrlList?.forEach((item) => {
styleContentList.push(customStyleSheetProcessor.styleMap.get(item)!);
});
const selfTemplateImportStr = this.dataGroup.otherMetaCollectionGroup[
'$self'
]
? ` `
: '';
const insertComponentData: ExportLibraryComponentMeta = {
id:
strings.classify(this.entryPoint) +
strings.classify(strings.camelize(componentDirName)),
className: componentClassName,
content:
selfTemplateImportStr +
this.dataGroup.outputContent.get(originFileName)!,
libraryPath: libraryPath,
useComponents: {
...getUseComponents(
useComponentPath.libraryPath,
useComponentPath.localPath,
this.entryPoint
),
...this.addGlobalTemplateService.getSelfUseComponents(),
},
moduleId: this.entryPoint,
};
if (styleContentList.length) {
insertComponentData.style = styleContentList.join('\n');
}
const list = changedData.content.split(/\n|\r\n/g);
list.splice(
Math.max(list.length - 1, 0),
0,
`let ${componentClassName}_${LIBRARY_COMPONENT_METADATA_SUFFIX}=${JSON.stringify(
insertComponentData
)}`
);
return list.join('\n');
}
}
================================================
FILE: src/builder/library/stylesheet-processor.ts
================================================
import { StylesheetProcessor } from 'ng-packagr/lib/styles/stylesheet-processor';
export class CustomStyleSheetProcessor extends StylesheetProcessor {
styleMap = new Map();
async process({
filePath,
content,
}: {
filePath: string;
content: string;
}): Promise {
const result = await super.process({ filePath, content });
this.styleMap.set(filePath, result);
return '';
}
}
================================================
FILE: src/builder/library/token.ts
================================================
import { InjectionToken } from 'static-injector';
export const RESOLVED_DATA_GROUP_TOKEN = new InjectionToken(
'RESOLVED_DATA_GROUP_TOKEN'
);
export const ENTRY_POINT_TOKEN = new InjectionToken(
'ENTRY_POINT_TOKEN'
);
export const ENTRY_FILE_TOKEN = new InjectionToken('ENTRY_FILE_TOKEN');
================================================
FILE: src/builder/library/type.ts
================================================
export interface ExtraTemplateData {
template: string;
useComponents?: Record;
templateName?: string;
outputPath?: string;
}
export interface ExportLibraryComponentMeta {
id: string;
content: string;
contentTemplate?: string;
style?: string;
className: string;
libraryPath: string;
useComponents: Record;
moduleId: string;
}
export interface LibraryComponentEntryMeta extends ExportLibraryComponentMeta {
importPath: string;
context: string;
contextPath: string;
}
================================================
FILE: src/builder/mini-program-compiler/component-compiler.service.ts
================================================
import type { R3ComponentMetadata } from '@angular/compiler';
import { Inject, Injectable } from 'static-injector';
import { BuildPlatform } from '../platform/platform';
import { COMPONENT_META } from '../token/component.token';
import { ComponentContext, TemplateDefinition } from './parse-node';
@Injectable()
export class ComponentCompilerService {
constructor(
private buildPlatform: BuildPlatform,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@Inject(COMPONENT_META) private componentMeta: R3ComponentMetadata,
private componentContext: ComponentContext
) {}
private collectionNode() {
const nodes = this.componentMeta.template.nodes;
const templateDefinition = new TemplateDefinition(
nodes,
this.componentContext
);
const list = templateDefinition.run();
return list.map((item) => item.getNodeMeta());
}
compile() {
const nodeList = this.collectionNode();
const result = this.buildPlatform.templateTransform.compile(nodeList);
return result;
}
}
================================================
FILE: src/builder/mini-program-compiler/index.ts
================================================
export * from './mini-program-compiler.service';
export * from './meta-collection';
export * from './type';
export * from './parse-node';
================================================
FILE: src/builder/mini-program-compiler/meta-collection.ts
================================================
import { UseComponent } from './type';
export class MetaCollection {
localPath: Set = new Set();
libraryPath: Set = new Set();
templateList: { name: string; content: string }[] = [];
merge(other: MetaCollection) {
other.localPath.forEach((item) => {
this.localPath.add(item);
});
other.libraryPath.forEach((item) => {
this.libraryPath.add(item);
});
this.templateList.push(...other.templateList);
}
}
================================================
FILE: src/builder/mini-program-compiler/mini-program-compiler.service.ts
================================================
import type {
R3ComponentMetadata,
R3DirectiveMetadata,
SelectorMatcher,
} from '@angular/compiler';
import type { NgtscProgram } from '@angular/compiler-cli';
import type { NgCompiler } from '@angular/compiler-cli/src/ngtsc/core';
import type {
ClassRecord,
TraitCompiler,
} from '@angular/compiler-cli/src/ngtsc/transform';
import { createCssSelectorForTs } from 'cyia-code-util';
import path from 'path';
import { Injectable, Injector } from 'static-injector';
import ts, { ClassDeclaration } from 'typescript';
import {
LIBRARY_COMPONENT_OUTPUT_PATH_SUFFIX,
LIBRARY_DIRECTIVE_LISTENERS_SUFFIX,
LIBRARY_DIRECTIVE_PROPERTIES_SUFFIX,
} from '../library';
import { BuildPlatform } from '../platform/platform';
import { COMPONENT_META } from '../token/component.token';
import { angularCompilerPromise, literalResolve } from '../util';
import { ComponentCompilerService } from './component-compiler.service';
import { MetaCollection } from './meta-collection';
import { ComponentContext } from './parse-node';
import {
ComponentMetaFromLibrary,
DirectiveMetaFromLibrary,
MetaFromLibrary,
ResolvedDataGroup,
UseComponent,
} from './type';
@Injectable()
export class MiniProgramCompilerService {
private ngCompiler!: NgCompiler;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private componentMap = new Map>();
private directiveMap = new Map();
private resolvedDataGroup: ResolvedDataGroup = {
style: new Map(),
outputContent: new Map(),
useComponentPath: new Map<
string,
{
localPath: UseComponent[];
libraryPath: UseComponent[];
}
>(),
otherMetaCollectionGroup: {},
};
constructor(
private ngTscProgram: NgtscProgram,
private injector: Injector,
private buildPlatform: BuildPlatform
) {}
init() {
this.ngCompiler = this.ngTscProgram.compiler;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const traitCompiler: TraitCompiler = (this.ngCompiler as any).compilation
.traitCompiler;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const classes = (traitCompiler as any).classes as Map<
ts.ClassDeclaration,
ClassRecord
>;
for (const [classDeclaration, classRecord] of classes) {
const fileName = classDeclaration.getSourceFile().fileName;
const componentTraits = classRecord.traits.filter(
(trait) => trait.handler.name === 'ComponentDecoratorHandler'
);
if (componentTraits.length > 1) {
throw new Error('组件装饰器异常');
}
componentTraits.forEach((trait) => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const meta: R3ComponentMetadata = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(trait as any).analysis?.meta,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
...(trait as any).resolution,
};
this.resolvedDataGroup.style.set(
path.normalize(fileName),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
((trait as any)?.analysis?.styleUrls || []).map(
(item: { url: string }) => this.resolveStyleUrl(fileName, item.url)
)
);
this.componentMap.set(
ts.getOriginalNode(classDeclaration) as ts.ClassDeclaration,
meta
);
});
const directiveTraits = classRecord.traits.filter(
(trait) => trait.handler.name === 'DirectiveDecoratorHandler'
);
if (directiveTraits.length > 1) {
throw new Error('指令装饰器异常');
}
directiveTraits.forEach((trait) => {
this.directiveMap.set(
ts.getOriginalNode(classDeclaration) as ts.ClassDeclaration,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(trait as any).analysis?.meta
);
});
}
}
async exportComponentBuildMetaMap() {
const { SelectorMatcher, CssSelector } = await angularCompilerPromise;
for (const [classDeclaration, meta] of this.componentMap) {
const fileName = path.normalize(
classDeclaration.getSourceFile().fileName
);
let directiveMatcher: SelectorMatcher | undefined;
// todo 这里断点
if (meta.declarations.length > 0) {
const matcher = new SelectorMatcher();
for (const directive of meta.declarations) {
const selector = directive.selector;
const directiveClassDeclaration = ts.getOriginalNode(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(directive as any).ref.node
) as ts.ClassDeclaration;
const directiveMeta = this.directiveMap.get(
directiveClassDeclaration
);
const componentMeta = this.componentMap.get(
directiveClassDeclaration
);
let libraryMeta: MetaFromLibrary | undefined;
if (directive.isComponent) {
libraryMeta = this.getLibraryComponentMeta(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(directive as any).ref.node
);
}
if (!directive.isComponent && !directiveMeta) {
libraryMeta = this.getLibraryDirectiveMeta(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(directive as any).ref.node
);
}
matcher.addSelectables(CssSelector.parse(selector), {
directive,
directiveMeta,
componentMeta,
libraryMeta,
});
}
directiveMatcher = matcher;
}
const componentBuildMeta = this.buildComponentMeta(
directiveMatcher,
meta
);
this.resolvedDataGroup.outputContent.set(
path.normalize(fileName),
componentBuildMeta.content
);
this.resolvedDataGroup.useComponentPath.set(
path.normalize(fileName),
componentBuildMeta.useComponentPath
);
for (const key in componentBuildMeta.otherMetaGroup) {
if (
Object.prototype.hasOwnProperty.call(
componentBuildMeta.otherMetaGroup,
key
)
) {
const element = componentBuildMeta.otherMetaGroup[key];
this.resolvedDataGroup.otherMetaCollectionGroup[key] =
this.resolvedDataGroup.otherMetaCollectionGroup[key] ||
new MetaCollection();
this.resolvedDataGroup.otherMetaCollectionGroup[key].merge(element);
}
}
}
this.resolvedDataGroup.useComponentPath.forEach((value, key) => {
value.libraryPath = Array.from(new Set(value.libraryPath));
value.localPath = Array.from(new Set(value.localPath));
});
return this.resolvedDataGroup;
}
private buildComponentMeta(
directiveMatcher: SelectorMatcher | undefined,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
componentMeta: R3ComponentMetadata
) {
const injector = Injector.create({
parent: this.injector,
providers: [
{ provide: ComponentCompilerService },
{ provide: COMPONENT_META, useValue: componentMeta },
{
provide: ComponentContext,
useFactory: () => {
return new ComponentContext(directiveMatcher);
},
},
],
});
const instance = injector.get(ComponentCompilerService);
return instance.compile();
}
private resolveStyleUrl(componentPath: string, styleUrl: string) {
return path.normalize(path.resolve(path.dirname(componentPath), styleUrl));
}
getDirectiveMap() {
return this.directiveMap;
}
getComponentMap() {
return this.componentMap;
}
private getLibraryDirectiveMeta(
classDeclaration: ts.ClassDeclaration
): DirectiveMetaFromLibrary | undefined {
let listeners: string[] = [];
let properties: string[] = [];
const directiveName = classDeclaration.name!.getText();
const selector = createCssSelectorForTs(classDeclaration.getSourceFile());
const listenersNode = selector.queryOne(
`VariableDeclaration[name=${directiveName}_${LIBRARY_DIRECTIVE_LISTENERS_SUFFIX}]`
) as ts.VariableDeclaration;
if (listenersNode) {
listeners = literalResolve(listenersNode.type!.getText());
}
const propertiesNode = selector.queryOne(
`VariableDeclaration[name=${directiveName}_${LIBRARY_DIRECTIVE_PROPERTIES_SUFFIX}]`
) as ts.VariableDeclaration;
if (propertiesNode) {
properties = literalResolve(propertiesNode.type!.getText());
}
return {
isComponent: false,
listeners: listeners,
properties: properties,
};
}
private getLibraryComponentMeta(
classDeclaration: ts.ClassDeclaration
): ComponentMetaFromLibrary | undefined {
const directiveName = classDeclaration.name!.getText();
const selector = createCssSelectorForTs(classDeclaration.getSourceFile());
const exportPathNode = selector.queryOne(
`VariableDeclaration[name=${directiveName}_${LIBRARY_COMPONENT_OUTPUT_PATH_SUFFIX}]`
) as ts.VariableDeclaration;
if (!exportPathNode) {
return undefined;
}
const exportPath = exportPathNode.type!.getText();
return {
exportPath: literalResolve(exportPath),
...this.getLibraryDirectiveMeta(classDeclaration)!,
isComponent: true,
};
}
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/bound-text.ts
================================================
import type { BoundText } from '../../angular-internal/ast.type';
import {
NgBoundTextMeta,
NgNodeKind,
NgNodeMeta,
ParsedNode,
} from './interface';
export class ParsedNgBoundText implements ParsedNode {
kind = NgNodeKind.BoundText;
constructor(
private node: BoundText,
public parent: ParsedNode | undefined,
public index: number
) {}
getNodeMeta(): NgBoundTextMeta {
return {
kind: NgNodeKind.BoundText,
index: this.index,
};
}
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/component-context.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import type {
R3ComponentMetadata,
R3DirectiveDependencyMetadata,
R3DirectiveMetadata,
SelectorMatcher,
} from '@angular/compiler';
import type {
ImportedFile,
Reference,
} from '@angular/compiler-cli/src/ngtsc/imports';
import { Injectable } from 'static-injector';
import ts from 'typescript';
import * as t from '../../angular-internal/ast.type';
import { createCssSelector } from '../../angular-internal/template';
import { getAttrsForDirectiveMatching } from '../../angular-internal/util';
import type { DirectiveMetaFromLibrary, MetaFromLibrary } from '../type';
import type { MatchedDirective, MatchedMeta } from './type';
@Injectable()
export class ComponentContext {
constructor(private directiveMatcher: SelectorMatcher | undefined) {}
matchDirective(node: t.Element): MatchedMeta[] {
if (!this.directiveMatcher) {
return [];
}
const name: string = node.name;
const selector = createCssSelector(
name,
getAttrsForDirectiveMatching(node)
);
const result: MatchedMeta[] = [];
this.directiveMatcher.match(
selector,
(
selector,
meta: {
directive: R3DirectiveDependencyMetadata & {
ref: Reference;
importedFile: ImportedFile;
};
componentMeta: R3ComponentMetadata;
directiveMeta: R3DirectiveMetadata;
libraryMeta: MetaFromLibrary;
}
) => {
let item: Partial;
const isComponent: boolean = !!meta.directive.isComponent;
if (isComponent) {
item = {
isComponent,
outputs: meta.directive.outputs,
filePath: (meta.directive.importedFile as ts.SourceFile).fileName,
selector: meta.directive.selector,
className: meta.directive.ref.node.name!.getText(),
listeners:
Object.keys(meta.componentMeta?.host?.listeners || {}) || [],
inputs: meta.directive.inputs,
};
if (meta.libraryMeta?.isComponent) {
item.exportPath = meta.libraryMeta.exportPath;
item.listeners = meta.libraryMeta.listeners;
item.properties = meta.libraryMeta.properties;
}
} else {
item = {
isComponent,
listeners:
Object.keys(meta.directiveMeta?.host?.listeners || {}) || [],
properties:
Object.keys(meta.directiveMeta?.host?.properties || {}) || [],
inputs: meta.directive.inputs,
outputs: meta.directive.outputs,
};
if (meta.libraryMeta && !meta.libraryMeta.isComponent) {
(item as MatchedDirective).listeners = (
meta.libraryMeta as DirectiveMetaFromLibrary
).listeners!;
(item as MatchedDirective).properties = (
meta.libraryMeta as DirectiveMetaFromLibrary
).properties!;
}
}
result.push(item as MatchedMeta);
}
);
return result;
}
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/content.ts
================================================
import type { Content } from '../../angular-internal/ast.type';
import { NgContentMeta, NgNodeKind, NgNodeMeta, ParsedNode } from './interface';
const SELECT_NAME_VALUE_REGEXP = /^\[slot=["']?([^"']*)["']?\]$/;
export class ParsedNgContent implements ParsedNode {
kind = NgNodeKind.Content;
constructor(
private node: Content,
public parent: ParsedNode | undefined,
public index: number
) {}
getNodeMeta(): NgContentMeta {
const nameAttr = this.node.attributes.find(
(item) => item.name === 'select'
);
let value: string | undefined;
if (nameAttr) {
const result = nameAttr.value.match(SELECT_NAME_VALUE_REGEXP);
if (!result) {
throw new Error(
`ng-content未匹配到指定格式的select,value:${nameAttr.value},需要格式为[slot="xxxx"]`
);
}
value = result[1];
}
return {
kind: NgNodeKind.Content,
name: value,
index: this.index,
};
}
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/element.ts
================================================
import type { Element } from '../../angular-internal/ast.type';
import { ComponentContext } from './component-context';
import { NgElementMeta, NgNodeKind, NgNodeMeta, ParsedNode } from './interface';
import type { MatchedComponent, MatchedDirective } from './type';
export class ParsedNgElement implements ParsedNode {
private tagName!: string;
private children: ParsedNode[] = [];
attributeObject: Record = {};
kind = NgNodeKind.Element;
inputs: string[] = [];
outputs: string[] = [];
singleClosedTag = false;
constructor(
private node: Element,
public parent: ParsedNode | undefined,
private componentMeta: MatchedComponent | undefined,
public index: number,
private directiveMeta: MatchedDirective | undefined
) {}
private analysis() {
this.getTagName();
this.node.attributes
.filter((item) => item.name !== 'class' && item.name !== 'style')
.forEach((item) => {
this.attributeObject[item.name] = item.value;
});
this.node.inputs.forEach((input) => {
if (input.type === 0) {
this.inputs.push(input.name);
}
});
this.node.outputs.forEach((output) => {
this.outputs.push(output.name);
});
if (
!this.node.endSourceSpan ||
this.node.startSourceSpan.end.offset ===
this.node.endSourceSpan.end.offset
) {
this.singleClosedTag = true;
}
}
private getTagName() {
const originTagName = this.node.name;
this.tagName = originTagName;
if (/^(div|p|h1|h2|h3|h4|h5|h6|span)$/.test(originTagName)) {
this.tagName = 'view';
} else if (originTagName === 'ng-container') {
this.tagName = 'block';
}
}
appendNgNodeChild(child: ParsedNode) {
this.children.push(child);
}
getNodeMeta(): NgElementMeta {
this.analysis();
return {
kind: NgNodeKind.Element,
tagName: this.tagName,
children: this.children.map((child) => child.getNodeMeta()),
inputs: this.inputs,
outputs: this.outputs,
attributes: this.attributeObject,
singleClosedTag: this.singleClosedTag,
componentMeta: this.componentMeta,
index: this.index,
directiveMeta: this.directiveMeta,
};
}
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/index.ts
================================================
export * from './component-context';
export * from './template-definition';
export * from './interface';
================================================
FILE: src/builder/mini-program-compiler/parse-node/interface.ts
================================================
import type { MatchedComponent, MatchedDirective } from './type';
export interface ParsedNode {
kind: NgNodeKind;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
parent: ParsedNode | undefined;
getNodeMeta(): T;
index: number;
}
export enum NgNodeKind {
Element,
BoundText,
Text,
Template,
Content,
}
export interface NgNodeMeta {
kind: NgNodeKind;
index: number;
}
export interface NgElementMeta extends NgNodeMeta {
kind: NgNodeKind.Element;
tagName: string;
children: NgNodeMeta[];
attributes: Record;
inputs: string[];
outputs: string[];
singleClosedTag: boolean;
componentMeta: MatchedComponent | undefined;
directiveMeta: MatchedDirective | undefined;
}
export interface NgBoundTextMeta extends NgNodeMeta {
kind: NgNodeKind.BoundText;
}
export interface NgTextMeta extends NgNodeMeta {
kind: NgNodeKind.Text;
value: string;
}
export interface NgTemplateMeta extends NgNodeMeta {
kind: NgNodeKind.Template;
children: NgNodeMeta[];
defineTemplateName: string;
}
export interface NgContentMeta extends NgNodeMeta {
kind: NgNodeKind.Content;
name: string | undefined;
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/template-definition.ts
================================================
import type {
AST,
AstVisitor,
Binary,
BindingPipe,
Call,
Chain,
Conditional,
ImplicitReceiver,
Interpolation,
KeyedRead,
KeyedWrite,
LiteralArray,
LiteralMap,
LiteralPrimitive,
NonNullAssert,
PrefixNot,
PropertyRead,
PropertyWrite,
SafeCall,
SafeKeyedRead,
SafePropertyRead,
Text,
TmplAstDeferredBlock,
TmplAstDeferredBlockError,
TmplAstDeferredBlockLoading,
TmplAstDeferredBlockPlaceholder,
TmplAstDeferredTrigger,
TmplAstForLoopBlock,
TmplAstForLoopBlockEmpty,
TmplAstIfBlock,
TmplAstIfBlockBranch,
TmplAstNode,
TmplAstRecursiveVisitor,
TmplAstSwitchBlock,
TmplAstSwitchBlockCase,
TmplAstUnknownBlock,
Visitor,
} from '@angular/compiler';
import * as t from '../../angular-internal/ast.type';
import { ParsedNgBoundText } from './bound-text';
import { ComponentContext } from './component-context';
import { ParsedNgContent } from './content';
import { ParsedNgElement } from './element';
import { NgNodeMeta, ParsedNode } from './interface';
import { ParsedNgTemplate } from './template';
import { ParsedNgText } from './text';
import { MatchedComponent, MatchedDirective } from './type';
export class TemplateDefinition implements TmplAstRecursiveVisitor {
private templateDefinitionMap = new Map();
private parentNode: ParsedNgElement | ParsedNgTemplate | undefined;
list: ParsedNode[] = [];
private declIndex = 0;
astVisitor = new CustomAstVisitor(() => {
this.declIndex++;
});
constructor(
private nodes: t.Node[],
private componentContext: ComponentContext
) {}
init() {}
visit?(node: t.Node) {}
visitElement(element: t.Element) {
const nodeIndex = this.declIndex++;
let componentMeta: MatchedComponent | undefined;
let directiveMeta: MatchedDirective | undefined;
const result = this.componentContext.matchDirective(element);
const directiveMetaList = result.filter((item) => {
if (item.isComponent) {
componentMeta = item;
}
return !item.isComponent;
});
if (directiveMetaList.length) {
directiveMeta = {
isComponent: false,
listeners: directiveMetaList.map((item) => item.listeners).flat(),
properties: directiveMetaList.map((item) => item.properties).flat(),
inputs: directiveMetaList.map((item) => item.inputs).flat(),
outputs: directiveMetaList.map((item) => item.outputs).flat(),
};
}
const instance = new ParsedNgElement(
element,
this.parentNode,
componentMeta,
nodeIndex,
directiveMeta
);
if (this.parentNode) {
this.parentNode.appendNgNodeChild(instance);
}
element.inputs.forEach((item) => {
item.value.visit(this.astVisitor);
});
const oldParent = this.parentNode;
this.parentNode = instance;
this.prepareRefsArray(element.references);
visitAll(this, element.children);
this.parentNode = oldParent;
if (!this.parentNode) {
this.list.push(instance);
}
}
visitTemplate(template: t.Template) {
const nodeIndex = this.declIndex++;
const templateInstance = new ParsedNgTemplate(
template,
this.parentNode,
nodeIndex
);
if (this.parentNode) {
this.parentNode.appendNgNodeChild(templateInstance);
}
this.prepareRefsArray(template.references);
template.templateAttrs.forEach((item) => {
if (typeof item.value !== 'string') {
item.value.visit(this.astVisitor);
}
});
template.inputs.forEach((item) => {
item.value.visit(this.astVisitor);
});
const instance = new TemplateDefinition(
template.children,
this.componentContext
);
instance.parentNode = templateInstance;
this.templateDefinitionMap.set(template, instance);
instance.run();
if (!this.parentNode) {
this.list.push(templateInstance);
}
}
visitContent(content: t.Content) {
const nodeIndex = this.declIndex++;
const instance = new ParsedNgContent(content, this.parentNode, nodeIndex);
if (this.parentNode) {
this.parentNode.appendNgNodeChild(instance);
} else {
this.list.push(instance);
}
}
visitVariable(variable: t.Variable) {}
visitReference(reference: t.Reference) {}
visitTextAttribute(attribute: t.TextAttribute) {}
visitBoundAttribute(attribute: t.BoundAttribute) {}
visitBoundEvent(attribute: t.BoundEvent) {}
visitText(text: t.Text) {
const nodeIndex = this.declIndex++;
const instance = new ParsedNgText(text, this.parentNode, nodeIndex);
if (this.parentNode) {
this.parentNode.appendNgNodeChild(instance);
} else {
this.list.push(instance);
}
}
visitBoundText(text: t.BoundText) {
const nodeIndex = this.declIndex++;
text.value.visit(this.astVisitor);
const instance = new ParsedNgBoundText(text, this.parentNode, nodeIndex);
if (this.parentNode) {
this.parentNode.appendNgNodeChild(instance);
} else {
this.list.push(instance);
}
}
visitIcu(icu: t.Icu) {}
run() {
visitAll(this, this.nodes);
return this.list;
}
prepareRefsArray(refs: t.Reference[]) {
if (!refs || !refs.length) {
return;
}
refs.forEach((item) => {
this.declIndex++;
});
}
// todo
visitDeferredBlock(deferred: TmplAstDeferredBlock): void {}
visitDeferredBlockError(block: TmplAstDeferredBlockError): void {}
visitDeferredBlockLoading(block: TmplAstDeferredBlockLoading): void {}
visitDeferredBlockPlaceholder(block: TmplAstDeferredBlockPlaceholder): void {}
visitDeferredTrigger(trigger: TmplAstDeferredTrigger): void {}
visitForLoopBlock(block: TmplAstForLoopBlock): void {}
visitForLoopBlockEmpty(block: TmplAstForLoopBlockEmpty): void {}
visitIfBlock(block: TmplAstIfBlock): void {}
visitIfBlockBranch(block: TmplAstIfBlockBranch): void {}
visitSwitchBlock(block: TmplAstSwitchBlock): void {}
visitSwitchBlockCase(block: TmplAstSwitchBlockCase): void {}
visitUnknownBlock(block: TmplAstUnknownBlock): void {}
}
export function visitAll(visitor: TemplateDefinition, nodes: TmplAstNode[]) {
for (const node of nodes) {
node.visit(visitor);
}
}
class CustomAstVisitor implements AstVisitor {
constructor(private pipeCallback: () => void) {}
visitCall(ast: Call) {
ast.receiver.visit(this);
this.visitAll(ast.args);
}
visitSafeCall(ast: SafeCall) {
ast.receiver.visit(this);
this.visitAll(ast.args);
}
visitSafeKeyedRead(ast: SafeKeyedRead) {
ast.receiver.visit(this);
ast.key.visit(this);
}
visitImplicitReceiver(ast: ImplicitReceiver) {}
visitInterpolation(ast: Interpolation) {
this.visitAll(ast.expressions);
}
visitKeyedRead(ast: KeyedRead) {
ast.receiver.visit(this);
ast.key.visit(this);
}
visitKeyedWrite(ast: KeyedWrite) {
ast.receiver.visit(this);
ast.key.visit(this);
ast.value.visit(this);
}
visitLiteralArray(ast: LiteralArray) {
this.visitAll(ast.expressions);
}
visitLiteralMap(ast: LiteralMap) {
this.visitAll(ast.values);
}
visitLiteralPrimitive(ast: LiteralPrimitive) {}
visitPipe(ast: BindingPipe) {
this.pipeCallback();
}
visitPrefixNot(ast: PrefixNot) {
ast.expression.visit(this);
}
visitNonNullAssert(ast: NonNullAssert) {
ast.expression.visit(this);
}
visitPropertyRead(ast: PropertyRead) {
ast.receiver.visit(this);
}
visitPropertyWrite(ast: PropertyWrite) {}
visitSafePropertyRead(ast: SafePropertyRead) {}
visitBinary(ast: Binary) {
ast.left.visit(this);
ast.right.visit(this);
}
visitChain(ast: Chain) {
this.visitAll(ast.expressions);
}
visitConditional(ast: Conditional) {
ast.condition.visit(this);
ast.trueExp.visit(this);
ast.falseExp.visit(this);
}
visit(ast: AST) {}
visitAll(asts: AST[]) {
for (let i = 0; i < asts.length; ++i) {
const original = asts[i];
original.visit(this);
}
}
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/template.ts
================================================
import type { Template } from '../../angular-internal/ast.type';
import {
NgNodeKind,
NgNodeMeta,
NgTemplateMeta,
ParsedNode,
} from './interface';
export class ParsedNgTemplate implements ParsedNode {
kind = NgNodeKind.Template;
private children: ParsedNode[] = [];
constructor(
private node: Template,
public parent: ParsedNode | undefined,
public index: number
) {}
appendNgNodeChild(child: ParsedNode) {
this.children.push(child);
}
private getTemplateName(): string {
if (this.node.references && this.node.references.length) {
return this.node.references[0].name;
} else {
return `ngDefault_${this.index}`;
}
}
getNodeMeta(): NgTemplateMeta {
const directive = this.getTemplateName()!;
const meta: NgTemplateMeta = {
kind: NgNodeKind.Template,
children: this.children.map((child) => child.getNodeMeta()),
index: this.index,
defineTemplateName: directive,
};
return meta;
}
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/text.ts
================================================
import type { Text } from '../../angular-internal/ast.type';
import { NgNodeKind, NgNodeMeta, NgTextMeta, ParsedNode } from './interface';
export class ParsedNgText implements ParsedNode {
kind = NgNodeKind.Text;
constructor(
private node: Text,
public parent: ParsedNode | undefined,
public index: number
) {}
getNodeMeta(): NgTextMeta {
return {
kind: NgNodeKind.Text,
value: this.node.value,
index: this.index,
};
}
}
================================================
FILE: src/builder/mini-program-compiler/parse-node/type.ts
================================================
export type MatchedMeta = MatchedComponent | MatchedDirective;
export interface MatchedComponent {
isComponent: true;
selector: string;
filePath: string;
exportPath: string;
className: string;
listeners: string[];
properties: string[];
inputs: string[];
outputs: string[];
}
export interface MatchedDirective {
isComponent: false;
listeners: string[];
properties: string[];
inputs: string[];
outputs: string[];
}
================================================
FILE: src/builder/mini-program-compiler/type.ts
================================================
import { MetaCollection } from './meta-collection';
export interface ComponentMetaFromLibrary {
isComponent: true;
exportPath: string;
listeners: string[];
properties: string[];
}
export interface DirectiveMetaFromLibrary {
isComponent: false;
listeners: string[];
properties: string[];
}
export type MetaFromLibrary =
| ComponentMetaFromLibrary
| DirectiveMetaFromLibrary;
export interface UseComponent {
selector: string;
className: string;
path: string;
}
export interface ResolvedDataGroup {
style: Map;
outputContent: Map;
useComponentPath: Map<
string,
{
localPath: UseComponent[];
libraryPath: UseComponent[];
}
>;
otherMetaCollectionGroup: Record;
}
================================================
FILE: src/builder/platform/bd/bdzn-platform.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
import { Injectable } from 'static-injector';
import { BuildPlatform } from '../platform';
import { BdZnTransform } from './bdzn.transform';
@Injectable()
export class BdZnBuildPlatform extends BuildPlatform {
packageName = 'bd';
globalObject = 'swan';
globalVariablePrefix = 'swan.__window';
fileExtname = {
style: '.css',
logic: '.js',
content: '.swan',
contentTemplate: '.swan',
};
importTemplate = `${fs
.readFileSync(path.resolve(__dirname, '../template/app-template.js'))
.toString()};
swan.__global = swan.__window = obj;`;
constructor(public templateTransform: BdZnTransform) {
super(templateTransform);
}
}
================================================
FILE: src/builder/platform/bd/bdzn.transform.ts
================================================
import { Injectable } from 'static-injector';
import { WxTransformLike } from '../template-transform-strategy/wx-like/wx-transform.base';
@Injectable()
export class BdZnTransform extends WxTransformLike {
override directivePrefix = 's';
override seq = '-';
override templateInterpolation: [string, string] = ['{{{', '}}}'];
}
================================================
FILE: src/builder/platform/dd/dd-platform.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
import { Injectable } from 'static-injector';
import { BuildPlatform } from '../platform';
import { DdTransform } from './dd.transform';
@Injectable()
export class DdBuildPlatform extends BuildPlatform {
packageName = 'dd';
globalObject = 'dd';
globalVariablePrefix = 'dd.__window';
fileExtname = {
style: '.acss',
logic: '.js',
content: '.axml',
contentTemplate: '.axml',
};
importTemplate = `${fs
.readFileSync(path.resolve(__dirname, '../template/app-template.js'))
.toString()};
dd.__global = dd.__window = obj;`;
constructor(public templateTransform: DdTransform) {
super(templateTransform);
}
}
================================================
FILE: src/builder/platform/dd/dd.transform.ts
================================================
import { Injectable } from 'static-injector';
import { WxTransformLike } from '../template-transform-strategy/wx-like/wx-transform.base';
@Injectable()
export class DdTransform extends WxTransformLike {
directivePrefix = 'a';
}
================================================
FILE: src/builder/platform/index.ts
================================================
export * from './platform-inject-config';
export * from './type';
export * from './platform';
================================================
FILE: src/builder/platform/jd/jd-platform.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
import { Injectable } from 'static-injector';
import { BuildPlatform } from '../platform';
import { JdTransform } from './jd.transform';
@Injectable()
export class JdBuildPlatform extends BuildPlatform {
packageName = 'jd';
globalObject = 'jd';
globalVariablePrefix = 'jd.__window';
fileExtname = {
style: '.jxss',
logic: '.js',
content: '.jxml',
contentTemplate: '.jxml',
};
importTemplate = `${fs
.readFileSync(path.resolve(__dirname, '../template/app-template.js'))
.toString()};
jd.__global = jd.__window = obj;`;
constructor(public templateTransform: JdTransform) {
super(templateTransform);
}
}
================================================
FILE: src/builder/platform/jd/jd.transform.ts
================================================
import { Injectable } from 'static-injector';
import { WxTransformLike } from '../template-transform-strategy/wx-like/wx-transform.base';
@Injectable()
export class JdTransform extends WxTransformLike {
directivePrefix = 'jd';
}
================================================
FILE: src/builder/platform/library/library-platform.ts
================================================
import { Injectable } from 'static-injector';
import { BuildPlatform } from '../platform';
import { LibraryTransform } from './library.transform';
export const ERROR_VALUE = '!!!library_can_not_use!!!';
@Injectable()
export class LibraryBuildPlatform extends BuildPlatform {
globalObject = ERROR_VALUE;
globalVariablePrefix = ERROR_VALUE;
fileExtname = {
style: '${fileExtname.style}',
logic: '${fileExtname.logic}',
content: '${fileExtname.content}',
contentTemplate: '${fileExtname.contentTemplate}',
};
importTemplate = ERROR_VALUE;
constructor(public templateTransform: LibraryTransform) {
super(templateTransform);
}
}
================================================
FILE: src/builder/platform/library/library.transform.ts
================================================
import { Injectable } from 'static-injector';
import {
EVENT_PREFIX_REGEXP,
WxTransformLike,
} from '../template-transform-strategy/wx-like/wx-transform.base';
@Injectable()
export class LibraryTransform extends WxTransformLike {
directivePrefix = '${directivePrefix}';
templateInterpolation: [string, string] = [
'${templateInterpolation[0]}',
'${templateInterpolation[1]}',
];
override eventListConvert = (list: string[]) => {
return `\${eventListConvert(${JSON.stringify(list)})}`;
};
}
================================================
FILE: src/builder/platform/platform-inject-config.ts
================================================
import { BdZnBuildPlatform } from './bd/bdzn-platform';
import { BdZnTransform } from './bd/bdzn.transform';
import { DdBuildPlatform } from './dd/dd-platform';
import { DdTransform } from './dd/dd.transform';
import { JdBuildPlatform } from './jd/jd-platform';
import { JdTransform } from './jd/jd.transform';
import { LibraryBuildPlatform } from './library/library-platform';
import { LibraryTransform } from './library/library.transform';
import { BuildPlatform, PlatformType } from './platform';
import { QqBuildPlatform } from './qq/qq-platform';
import { QqTransform } from './qq/qq.transform';
import { WxBuildPlatform } from './wx/wx-platform';
import { WxTransform } from './wx/wx.transform';
import { ZfbBuildPlatform } from './zfb/zfb-platform';
import { ZfbTransform } from './zfb/zfb.transform';
import { ZjBuildPlatform } from './zjtd/zj-platform';
import { ZjTransform } from './zjtd/zj.transform';
export function getBuildPlatformInjectConfig(platform: PlatformType) {
switch (platform) {
case PlatformType.wx:
return [
{ provide: WxTransform },
{ provide: WxBuildPlatform },
{ provide: BuildPlatform, useClass: WxBuildPlatform },
];
case PlatformType.zj:
return [
{ provide: ZjTransform },
{ provide: ZjBuildPlatform },
{ provide: BuildPlatform, useClass: ZjBuildPlatform },
];
case PlatformType.jd:
return [
{ provide: JdTransform },
{ provide: JdBuildPlatform },
{ provide: BuildPlatform, useClass: JdBuildPlatform },
];
case PlatformType.bdzn:
return [
{ provide: BdZnTransform },
{ provide: BdZnBuildPlatform },
{ provide: BuildPlatform, useClass: BdZnBuildPlatform },
];
case PlatformType.zfb:
return [
{ provide: ZfbTransform },
{ provide: ZfbBuildPlatform },
{ provide: BuildPlatform, useClass: ZfbBuildPlatform },
];
case PlatformType.qq:
return [
{ provide: QqTransform },
{ provide: QqBuildPlatform },
{ provide: BuildPlatform, useClass: QqBuildPlatform },
];
case PlatformType.dd:
return [
{ provide: DdTransform },
{ provide: DdBuildPlatform },
{ provide: BuildPlatform, useClass: DdBuildPlatform },
];
case PlatformType.library:
return [
{ provide: LibraryTransform },
{ provide: LibraryBuildPlatform },
{ provide: BuildPlatform, useClass: LibraryBuildPlatform },
];
default:
throw new Error('未能匹配到相关平台');
}
}
================================================
FILE: src/builder/platform/platform.ts
================================================
import { Injectable } from 'static-injector';
import { TemplateTransformBase } from './template-transform-strategy/transform.base';
import { PlatformFileExtname } from './type';
export enum PlatformType {
wx = 'wx',
zj = 'zj',
jd = 'jd',
bdzn = 'bdzn',
zfb = 'zfb',
qq = 'qq',
dd = 'dd',
/** 这个属性只会在内部被使用 */
library = 'library',
}
@Injectable()
export class BuildPlatform {
packageName!: string;
globalObject!: string;
globalVariablePrefix!: string;
fileExtname!: PlatformFileExtname;
importTemplate!: string;
constructor(public templateTransform: TemplateTransformBase) {
this.templateTransform.init();
}
}
================================================
FILE: src/builder/platform/qq/qq-platform.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
import { Injectable } from 'static-injector';
import { BuildPlatform } from '../platform';
import { QqTransform } from './qq.transform';
@Injectable()
export class QqBuildPlatform extends BuildPlatform {
packageName = 'qq';
globalObject = 'qq';
globalVariablePrefix = 'qq.__window';
fileExtname = {
style: '.qss',
logic: '.js',
content: '.qml',
contentTemplate: '.qml',
};
importTemplate = `${fs
.readFileSync(path.resolve(__dirname, '../template/app-template.js'))
.toString()};
qq.__global = qq.__window = obj;`;
constructor(public templateTransform: QqTransform) {
super(templateTransform);
}
}
================================================
FILE: src/builder/platform/qq/qq.transform.ts
================================================
import { Injectable } from 'static-injector';
import { WxTransformLike } from '../template-transform-strategy/wx-like/wx-transform.base';
@Injectable()
export class QqTransform extends WxTransformLike {
directivePrefix = 'qq';
}
================================================
FILE: src/builder/platform/template/app-template.js
================================================
const obj = {
Zone: typeof Zone !== 'undefined' && Zone,
setTimeout: typeof setTimeout !== 'undefined' && setTimeout,
clearTimeout:
typeof clearTimeout !== 'undefined' &&
function (id) {
return clearTimeout(id);
},
setInterval: typeof setInterval !== 'undefined' && setInterval,
clearInterval:
typeof clearInterval !== 'undefined' &&
function (id) {
return clearInterval(id);
},
Promise: typeof Promise !== 'undefined' && Promise,
Reflect: typeof Reflect !== 'undefined' && Reflect,
requestAnimationFrame:
typeof requestAnimationFrame !== 'undefined' && requestAnimationFrame,
cancelAnimationFrame:
typeof cancelAnimationFrame !== 'undefined' &&
function (id) {
return cancelAnimationFrame(id);
},
performance: typeof performance !== 'undefined' && performance,
navigator: typeof navigator !== 'undefined' && navigator,
// 来自 queue-microtask 因为引入太麻烦直接复制了
queueMicrotask:typeof queueMicrotask === 'function'
? queueMicrotask.bind(typeof window !== 'undefined' ? window : global)
// reuse resolved promise, and allocate it lazily
: cb => (promise || (promise = Promise.resolve()))
.then(cb)
.catch(err => setTimeout(() => { throw err }, 0))
};
================================================
FILE: src/builder/platform/template-transform-strategy/transform.base.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Injectable } from 'static-injector';
import type { NgNodeMeta } from '../../mini-program-compiler';
import { MetaCollection, UseComponent } from '../../mini-program-compiler';
@Injectable()
export abstract class TemplateTransformBase {
abstract init(): any;
abstract compile(nodes: NgNodeMeta[]): {
content: string;
useComponentPath: {
localPath: UseComponent[];
libraryPath: UseComponent[];
};
otherMetaGroup: Record;
};
abstract getData(): any;
abstract templateInterpolation: [string, string];
abstract eventListConvert: (list: string[]) => string;
}
================================================
FILE: src/builder/platform/template-transform-strategy/wx-like/wx-container.ts
================================================
import type {
NgBoundTextMeta,
NgContentMeta,
NgElementMeta,
NgNodeMeta,
NgTemplateMeta,
NgTextMeta,
} from '../../../mini-program-compiler';
import { MetaCollection } from '../../../mini-program-compiler';
import {
isNgBoundTextMeta,
isNgContentMeta,
isNgElementMeta,
isNgTemplateMeta,
isNgTextMeta,
} from '../../util/type-predicate';
export interface WxContainerGlobalConfig {
seq: string;
directivePrefix: string;
eventListConvert: (name: string[]) => string;
templateInterpolation: [string, string];
}
export class WxContainer {
private templateStr: string = '';
private childContainerList: WxContainer[] = [];
fromTemplate!: string;
defineTemplateName!: string;
private metaCollection: MetaCollection = new MetaCollection();
constructor(private parent?: WxContainer) {}
private _compileTemplate(node: NgNodeMeta): string {
if (isNgElementMeta(node)) {
return this.ngElementTransform(node);
} else if (isNgBoundTextMeta(node)) {
return this.ngBoundTextTransform(node);
} else if (isNgTextMeta(node)) {
return this.ngTextTransform(node);
} else if (isNgContentMeta(node)) {
return this.ngContentTransform(node);
} else if (isNgTemplateMeta(node)) {
return this.ngTemplateTransform(node);
} else {
throw new Error('未知的ng节点元数据');
}
}
compileNode(node: NgNodeMeta) {
this.templateStr += this._compileTemplate(node);
}
private ngElementTransform(node: NgElementMeta): string {
if (node.componentMeta) {
if (node.componentMeta.exportPath) {
this.metaCollection.libraryPath.add({
selector: node.componentMeta.selector,
path: node.componentMeta.exportPath,
className: node.componentMeta.className,
});
} else {
this.metaCollection.localPath.add({
path: node.componentMeta.filePath,
selector: node.componentMeta.selector,
className: node.componentMeta.className,
});
}
}
const children = node.children.map((child) => this._compileTemplate(child));
const commonTagProperty = `${this.setComponentIdentification(
node.componentMeta?.isComponent,
node.index
)} ${this.elementPropertyAndEvent(node, node.index).join(' ')}`;
if (node.singleClosedTag) {
return `<${node.tagName} ${commonTagProperty}/>`;
}
return `<${node.tagName} ${
node.tagName === 'block' ? '' : commonTagProperty
}>${children.join('')}${node.tagName}>`;
}
private ngBoundTextTransform(node: NgBoundTextMeta): string {
return `{{nodeList[${node.index}].value}}`;
}
private ngContentTransform(node: NgContentMeta): string {
return node.name ? ` ` : ` `;
}
private ngTemplateTransform(node: NgTemplateMeta): string {
let content = '';
const defineTemplateName = node.defineTemplateName;
const childContainer = new WxContainer(this);
const globalTemplate = this.isGlobalTemplate(node.defineTemplateName);
if (globalTemplate) {
if (this.fromTemplate && this.fromTemplate !== globalTemplate) {
throw new Error(
`全局ng-template中不可包含其他位置的ng-template,当前为${this.fromTemplate},包含${globalTemplate}`
);
} else if (globalTemplate) {
childContainer.fromTemplate = globalTemplate;
childContainer.defineTemplateName = defineTemplateName;
}
} else {
childContainer.fromTemplate = this.fromTemplate;
childContainer.defineTemplateName = defineTemplateName;
}
this.childContainerList.push(childContainer);
node.children.forEach((childNode) => {
childContainer.compileNode(childNode);
});
if (this.fromTemplate === childContainer.fromTemplate) {
this.metaCollection.templateList.push({
name: defineTemplateName,
content: `${childContainer.templateStr} `,
});
}
content += `
`;
return content;
}
private ngTextTransform(node: NgTextMeta): string {
return `${node.value}`;
}
private getTemplateDataStr(directiveIndex: number, indexName: string) {
return `data="${WxContainer.globalConfig.templateInterpolation[0]}...nodeList[${directiveIndex}][${indexName}] ${WxContainer.globalConfig.templateInterpolation[1]}"`;
}
export(): { wxmlTemplate: string } {
return {
wxmlTemplate: this.templateStr,
};
}
private setComponentIdentification(
isComponent: boolean | undefined,
nodeIndex: number | undefined
) {
if (isComponent) {
return `nodePath="{{nodePath}}" nodeIndex="${nodeIndex}"`;
}
return ``;
}
private elementPropertyAndEvent(node: NgElementMeta, index: number) {
const propertyMap = new Map();
const attributeMap = new Map();
propertyMap.set('class', `nodeList[${index}].class`);
propertyMap.set('style', `nodeList[${index}].style`);
Object.entries(node.attributes)
.filter(([key, value]) => value !== '')
.forEach(([key, value]) => {
attributeMap.set(key, value);
});
node.inputs
.filter(
(property) =>
!(
(node.componentMeta?.inputs?.includes(property) ||
node.directiveMeta?.inputs?.includes(property)) &&
!(
node.directiveMeta?.properties?.includes(property) ||
node.componentMeta?.properties?.includes(property)
)
)
)
.filter((key) => !/^(class\.?|style\.?)/.test(key))
.forEach((key) => {
propertyMap.set(key, `nodeList[${index!}].property.${key}`);
});
[
...(node.directiveMeta?.properties || []),
...(node.componentMeta?.properties || []),
]
.filter((key) => !/^(class\.?|style\.?)/.test(key))
.forEach((key) => {
propertyMap.set(key, `nodeList[${index!}].property.${key}`);
});
const eventList: string[] = [
...node.outputs.filter(
(item) =>
!(
node.componentMeta?.outputs.some((output) => output === item) ||
node.directiveMeta?.outputs.some((output) => output === item)
)
),
...(node.directiveMeta?.listeners || []),
...(node.componentMeta?.isComponent ? node.componentMeta.listeners : []),
];
const result = WxContainer.globalConfig.eventListConvert(eventList);
if (result) {
propertyMap.set(`data-node-path`, `nodePath`);
propertyMap.set(`data-node-index`, `${index}`);
}
return [
...Array.from(attributeMap.entries()).map(
([key, value]) => `${key}="${value}"`
),
...Array.from(propertyMap.entries()).map(
([key, value]) => `${key}="{{${value}}}"`
),
result,
];
}
static globalConfig: WxContainerGlobalConfig;
static initWxContainerFactory(globalConfig: WxContainerGlobalConfig) {
this.globalConfig = globalConfig;
}
private isGlobalTemplate(name: string) {
const result = name.match(/^\$\$mp\$\$([^$]+)\$\$(.*)/);
if (!result) {
return undefined;
}
return result[1];
}
exportMetaCollectionGroup() {
const obj: Record = {};
if (!this.fromTemplate) {
obj.$inline = obj.$inline || new MetaCollection();
obj.$inline.merge(this.metaCollection);
} else if (this.fromTemplate == '__self__') {
obj.$self = obj.$self || new MetaCollection();
obj.$self.merge(this.metaCollection);
obj.$self.templateList.push({
name: this.defineTemplateName,
content: `${this.templateStr} `,
});
} else {
obj[this.fromTemplate] = obj[this.fromTemplate] || new MetaCollection();
obj[this.fromTemplate].merge(this.metaCollection);
obj[this.fromTemplate].templateList.push({
name: this.defineTemplateName,
content: `${this.templateStr} `,
});
}
this.childContainerList.forEach((container) => {
const result = container.exportMetaCollectionGroup();
for (const key in result) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
const element = result[key];
obj[key] = obj[key] || new MetaCollection();
obj[key].merge(element);
}
}
});
return obj;
}
}
================================================
FILE: src/builder/platform/template-transform-strategy/wx-like/wx-transform.base.ts
================================================
import { strings } from '@angular-devkit/core';
import type { NgNodeMeta } from '../../../mini-program-compiler';
import { TemplateTransformBase } from '../transform.base';
import { WxContainer } from './wx-container';
export const EVENT_PREFIX_REGEXP =
/^(bind|catch|mut-bind|capture-bind|capture-catch)(.*)$/;
export abstract class WxTransformLike extends TemplateTransformBase {
seq = ':';
templateInterpolation: [string, string] = ['{{', '}}'];
abstract directivePrefix: string;
constructor() {
super();
}
init() {
WxContainer.initWxContainerFactory({
seq: this.seq,
directivePrefix: this.directivePrefix,
eventListConvert: this.eventListConvert,
templateInterpolation: this.templateInterpolation,
});
}
compile(nodes: NgNodeMeta[]) {
const container = new WxContainer();
nodes.forEach((node) => {
container.compileNode(node);
});
const result = container.export();
const metaCollectionGroup = container.exportMetaCollectionGroup();
const inlineMetaCollection = metaCollectionGroup.$inline;
delete metaCollectionGroup.$inline;
return {
content: `${inlineMetaCollection.templateList
.map((item) => item.content)
.join('')}${
result.wxmlTemplate
} `,
useComponentPath: {
localPath: [...inlineMetaCollection.localPath],
libraryPath: [...inlineMetaCollection.libraryPath],
},
otherMetaGroup: metaCollectionGroup,
};
}
getData() {
return { directivePrefix: this.directivePrefix };
}
eventNameConvert(tagEventMeta: string) {
const result = tagEventMeta.match(EVENT_PREFIX_REGEXP);
let prefix: string = 'bind';
let type: string = tagEventMeta;
if (result) {
prefix = result[1];
type = result[2];
}
return {
prefix,
type,
name: `${prefix}:${type}`,
};
}
eventListConvert = (list: string[]) => {
const eventMap = new Map();
list.forEach((eventName) => {
const result = this.eventNameConvert(eventName);
const prefix = strings.camelize(result.prefix);
const bindEventName = `${prefix}Event`;
if (eventMap.has(result.name)) {
if (eventMap.get(result.name) === bindEventName) {
return;
} else {
throw new Error(
`事件名[${result.name}]解析异常,原绑定${eventMap.get(
result.name
)},现绑定${bindEventName}`
);
}
}
eventMap.set(result.name, bindEventName);
});
return Array.from(eventMap.entries())
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
};
}
================================================
FILE: src/builder/platform/type.ts
================================================
export interface PlatformFileExtname {
style: string;
logic: string;
content: string;
contentTemplate: string;
config?: string;
}
================================================
FILE: src/builder/platform/util/dataset-bind.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
export class DatasetBind {
constructor(private data: any) {}
toJSON() {
return this.objectToJSON(this.data);
}
private objectToJSON(obj: Record) {
let str = '{';
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const element = obj[key];
str += `${key}:`;
if (typeof element === 'string') {
str += `${this.stringToJSON(element)}`;
} else if (typeof element === 'number') {
str += `${this.numberToJSON(element)}`;
} else if (element instanceof Array) {
str += `${this.arrayToJSON(element)}`;
} else if (typeof element === 'object' && element !== null) {
str += `${this.objectToJSON(element as any)}`;
}
str += ',';
}
}
str = str.substr(0, str.length - 1);
str += '}';
return str;
}
private numberToJSON(number: number) {
return `${number}`;
}
private stringToJSON(string: string) {
return `'${string}'`;
}
private arrayToJSON(list: any[]) {
let str = '[';
for (let i = 0; i < list.length; i++) {
const element = list[i];
if (typeof element === 'string') {
str += this.stringToJSON(element);
} else if (typeof element === 'number') {
str += this.numberToJSON(element);
} else if (element instanceof Array) {
str += this.arrayToJSON(element);
} else if (typeof element === 'object' && element !== null) {
str += this.objectToJSON(element as any);
}
if (i !== list.length - 1) {
str += ',';
}
}
str += ']';
return str;
}
}
================================================
FILE: src/builder/platform/util/type-predicate.ts
================================================
import {
NgBoundTextMeta,
NgContentMeta,
NgElementMeta,
NgNodeKind,
NgNodeMeta,
NgTemplateMeta,
NgTextMeta,
} from '../../mini-program-compiler';
export function isNgElementMeta(node: NgNodeMeta): node is NgElementMeta {
return node.kind === NgNodeKind.Element;
}
export function isNgBoundTextMeta(node: NgNodeMeta): node is NgBoundTextMeta {
return node.kind === NgNodeKind.BoundText;
}
export function isNgContentMeta(node: NgNodeMeta): node is NgContentMeta {
return node.kind === NgNodeKind.Content;
}
export function isNgTemplateMeta(node: NgNodeMeta): node is NgTemplateMeta {
return node.kind === NgNodeKind.Template;
}
export function isNgTextMeta(node: NgNodeMeta): node is NgTextMeta {
return node.kind === NgNodeKind.Text;
}
================================================
FILE: src/builder/platform/wx/wx-platform.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
import { Injectable } from 'static-injector';
import { BuildPlatform } from '../platform';
import { WxTransform } from './wx.transform';
@Injectable()
export class WxBuildPlatform extends BuildPlatform {
packageName = 'wx';
globalObject = 'wx';
globalVariablePrefix = 'wx.__window';
fileExtname = {
style: '.wxss',
logic: '.js',
content: '.wxml',
contentTemplate: '.wxml',
};
importTemplate = `${fs
.readFileSync(path.resolve(__dirname, '../template/app-template.js'))
.toString()};
wx.__global = wx.__window = obj;`;
constructor(public templateTransform: WxTransform) {
super(templateTransform);
}
}
================================================
FILE: src/builder/platform/wx/wx.transform.ts
================================================
import { Injectable } from 'static-injector';
import { WxTransformLike } from '../template-transform-strategy/wx-like/wx-transform.base';
@Injectable()
export class WxTransform extends WxTransformLike {
directivePrefix = 'wx';
}
================================================
FILE: src/builder/platform/zfb/zfb-platform.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
import { Injectable } from 'static-injector';
import { BuildPlatform } from '../platform';
import { ZfbTransform } from './zfb.transform';
@Injectable()
export class ZfbBuildPlatform extends BuildPlatform {
packageName = 'zfb';
globalObject = 'my';
globalVariablePrefix = 'my.__window';
fileExtname = {
style: '.acss',
logic: '.js',
content: '.axml',
contentTemplate: '.axml',
};
importTemplate = `${fs
.readFileSync(path.resolve(__dirname, '../template/app-template.js'))
.toString()};
my.__global = my.__window = obj;`;
constructor(public templateTransform: ZfbTransform) {
super(templateTransform);
}
}
================================================
FILE: src/builder/platform/zfb/zfb.transform.ts
================================================
import { capitalize } from '@angular-devkit/core/src/utils/strings';
import { Injectable } from 'static-injector';
import { WxTransformLike } from '../template-transform-strategy/wx-like/wx-transform.base';
const BIND_PREFIX_REGEXP = /^(bind|mut-bind|capture-bind)(.*)/;
const CATCH_PREFIX_REGEXP = /^(catch|capture-catch)(.*)/;
@Injectable()
export class ZfbTransform extends WxTransformLike {
directivePrefix = 'a';
override eventNameConvert(name: string) {
let result = name.match(BIND_PREFIX_REGEXP);
if (result) {
return {
prefix: 'on',
type: result[2],
name: `on${capitalize(result[2])}`,
};
}
result = name.match(CATCH_PREFIX_REGEXP);
if (result) {
return {
prefix: 'catch',
type: result[2],
name: `catch${capitalize(result[2])}`,
};
}
return {
prefix: 'on',
type: name,
name: `on${capitalize(name)}`,
};
}
}
================================================
FILE: src/builder/platform/zjtd/zj-platform.ts
================================================
import * as fs from 'fs-extra';
import * as path from 'path';
import { Injectable } from 'static-injector';
import { BuildPlatform } from '../platform';
import { ZjTransform } from './zj.transform';
/** 字节小程序适配 */
@Injectable()
export class ZjBuildPlatform extends BuildPlatform {
packageName = 'zjtd';
globalObject = 'tt';
globalVariablePrefix = 'tt.__window';
fileExtname = {
style: '.ttss',
logic: '.js',
content: '.ttml',
contentTemplate: '.ttml',
config: '.json',
};
importTemplate = `${fs
.readFileSync(path.resolve(__dirname, '../template/app-template.js'))
.toString()};
tt.__global = tt.__window = obj;`;
constructor(public templateTransform: ZjTransform) {
super(templateTransform);
}
}
================================================
FILE: src/builder/platform/zjtd/zj.transform.ts
================================================
import { Injectable } from 'static-injector';
import { WxTransformLike } from '../template-transform-strategy/wx-like/wx-transform.base';
@Injectable()
export class ZjTransform extends WxTransformLike {
directivePrefix = 'tt';
}
================================================
FILE: src/builder/test/fixture/watch/sub3/sub3.component.html
================================================
测试
================================================
FILE: src/builder/test/fixture/watch/sub3/sub3.component.ts
================================================
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-sub3',
templateUrl: './sub3.component.html',
})
export class Sub3Component implements OnInit {
constructor() {}
ngOnInit() {}
}
================================================
FILE: src/builder/test/fixture/watch/sub3/sub3.entry.ts
================================================
import { pageStartup } from 'angular-miniprogram';
import { Sub3Component } from './sub3.component';
import { Sub3Module } from './sub3.module';
pageStartup(Sub3Module, Sub3Component);
================================================
FILE: src/builder/test/fixture/watch/sub3/sub3.module.ts
================================================
import { NgModule } from '@angular/core';
import { CommonModule } from 'angular-miniprogram/common';
import { Sub3Component } from './sub3.component';
@NgModule({
imports: [CommonModule],
declarations: [Sub3Component],
})
export class Sub3Module {}
================================================
FILE: src/builder/token/component.token.ts
================================================
import { InjectionToken } from 'static-injector';
export const COMPONENT_META = new InjectionToken('COMPONENT_META');
================================================
FILE: src/builder/util/index.ts
================================================
export * from './library-template-scope-name';
export * from './literal-resolve';
export * from './load_esm';
export * from './raw-updater';
export * from './run-script';
================================================
FILE: src/builder/util/library-template-scope-name.ts
================================================
import { strings } from '@angular-devkit/core';
export function libraryTemplateScopeName(library: string) {
return strings.classify(library.replace(/[@/]/g, ''));
}
================================================
FILE: src/builder/util/literal-resolve.ts
================================================
/* eslint-disable @typescript-eslint/no-explicit-any */
import { runScript } from './run-script';
export function literalResolve>(
content: string,
options?: T
) {
return runScript(`(()=>{return ${content}})()`, options);
}
================================================
FILE: src/builder/util/load_esm.ts
================================================
async function loadEsmModule(modulePath: string): Promise {
const namespaceObject = await new Function(
'modulePath',
`return import(modulePath);`
)(modulePath);
// If it is not ESM then the values needed will be stored in the `default` property.
// TODO_ESM: This can be removed once `@angular/*` packages are ESM only.
if (namespaceObject.default) {
return namespaceObject.default;
} else {
return namespaceObject;
}
}
export const angularCompilerPromise =
loadEsmModule('@angular/compiler');
export const angularCompilerCliPromise = loadEsmModule<
typeof import('@angular/compiler-cli')
>('@angular/compiler-cli');
================================================
FILE: src/builder/util/raw-updater.spec.ts
================================================
import { DeleteChange, InsertChange, ReplaceChange } from 'cyia-code-util';
import { RawUpdater } from './raw-updater';
const originContent = '123456789';
describe('raw-updater', () => {
it('default', () => {
const result = RawUpdater.update(originContent, []);
expect(result).toBe(originContent);
});
it('insert', () => {
const result = RawUpdater.update(originContent, [
new InsertChange(0, '000'),
new InsertChange(5, '000'),
new InsertChange(8, '000'),
]);
expect(result).toBe(`000123450006780009`);
});
it('delete', () => {
const result = RawUpdater.update(originContent, [
new DeleteChange(0, 1),
new DeleteChange(5, 1),
new DeleteChange(8, 1),
]);
expect(result).toBe(`234578`);
});
it('replace', () => {
const result = RawUpdater.update(originContent, [
new ReplaceChange(0, 1, '000'),
new ReplaceChange(5, 1, '000'),
new ReplaceChange(8, 1, '000'),
]);
expect(result).toBe(`000234500078000`);
});
it('混合', () => {
const result = RawUpdater.update(originContent, [
new InsertChange(0, '000'),
new DeleteChange(5, 1),
new ReplaceChange(8, 1, '000'),
]);
expect(result).toBe(`0001234578000`);
});
});
================================================
FILE: src/builder/util/raw-updater.ts
================================================
import {
Change,
DeleteChange,
InsertChange,
ReplaceChange,
} from 'cyia-code-util';
export class RawUpdater {
static update(content: string, change: Change[]) {
change = change.sort((a, b) => b.start - a.start);
return new RawUpdater(content, change).update();
}
originContent!: string;
constructor(private content: string, private changes: Change[]) {
this.originContent = content;
}
update() {
this.changes.forEach((change) => {
let deleteChange: DeleteChange | undefined;
let insertChange: InsertChange | undefined;
if (change instanceof ReplaceChange) {
insertChange = new InsertChange(change.start, change.content);
deleteChange = new DeleteChange(change.start, change.length);
} else if (change instanceof InsertChange) {
insertChange = change;
} else if (change instanceof DeleteChange) {
deleteChange = change;
}
let list!: [string, string];
if (deleteChange) {
list = this.slice(deleteChange.start, deleteChange.length);
}
if (!list && insertChange) {
list = this.slice(insertChange.start);
list.splice(1, 0, insertChange.content);
} else if (insertChange) {
list.splice(1, 0, insertChange.content);
}
this.content = list.join('');
});
return this.content;
}
private slice(pos: number, length: number = 0): [string, string] {
return [
this.content.substring(0, pos),
this.content.substring(pos + length),
];
}
}
================================================
FILE: src/builder/util/run-script.ts
================================================
/* eslint-disable no-console */
import vm from 'vm';
export function runScript(code: string, context?: vm.Context) {
try {
return vm.runInNewContext(code, context);
} catch (error) {
console.error('运行脚本错误');
}
}
================================================
FILE: src/library/common/.gitignore
================================================
*
!.gitignore
!ng-package.json
!/http
!/http/ng-package.json
================================================
FILE: src/library/common/ng-package.json
================================================
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "./index.ts"
}
}
================================================
FILE: src/library/declaration/index.d.ts
================================================
declare const ngDevMode: null | any;
declare const Zone: any
================================================
FILE: src/library/forms/.gitignore
================================================
*
!.gitignore
!/src
!/src/directives
!/src/directives/default_value_accessor.ts
!/src/directives/checkbox_value_accessor.ts
!/src/directives/radio_control_value_accessor.ts
!/src/directives.ts
!/readme.md
!/src/directives/picker_view_value_accessor.ts
!/src/directives/switch_value_accessor.ts
!/src/directives/slider_value_accessor.ts
!/src/directives/picker_value_accessor.ts
!/src/forms.ts
!/ng-package.json
================================================
FILE: src/library/forms/ng-package.json
================================================
{
"$schema": "../../../node_modules/ng-packagr/ng-package.schema.json",
"lib": {
"entryFile": "./index.ts"
}
}
================================================
FILE: src/library/forms/readme.md
================================================
# 表单逻辑
| 组件名 | 输入属性 | 事件 | 禁用 | 备注 |
| -------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------ | ---------- | ----------------------------------------------------------------------- |
| input | value`string` | bindinput bindfocus bindblur | disabled | 事件比较多,可以对比参考 ,text,number,idcard,digit,safe-password 类型 |
| textarea | value`string` | bindfocus bindblur bindinput | disabled |
| checkbox | `checked` boolean | 触发`checkbox-group`的 `change` 自身无事件 | `disabled` | `value`在触发时携带 |
| checkbox-group | | | | 只用于`checkbox` |
| editor | | | | 貌似不支持双向绑定 |
| picker | mode 对应, selector:value`number`, multiSelector:value`[]`, time:value`string hh:mm`, data:value`string YYYY-MM-DD`, region:value`[]` | bindchange | disabled |
| picker-view | value`number[]`组 | bindchange | 无禁用 |
| radio | checked | 自身无事件 | disabled | 需要配合 |
| radio-group | | bindchange | |
| slider | value`number` | bindchange | disabled |
| switch | checked`boolean` | bindchange | disabled |
# 开发状态
- input 已实现
- switch 已实现
- slider 已实现
- radio 已实现
- checkbox 已实现
- picker 已实现
# 待实现
- 双向绑定后,变更检测触发有点问题
# 修改文件
- 双向绑定部分,修改和添加了部分组件的双向绑定支持以适应小程序.
- forms/directives 改了导出导入声明,用来添加修改移除双向绑定
# 不同平台
- 如果出现不同平台,并且不同平台有不同的模块绑定逻辑,那么这里将不做适配,转为第三方适配
================================================
FILE: src/library/forms/src/directives/checkbox_value_accessor.ts
================================================
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
ContentChildren,
Directive,
ElementRef,
HostBinding,
Input,
QueryList,
Renderer2,
forwardRef,
} from '@angular/core';
import {
BuiltInControlValueAccessor,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
} from './control_value_accessor';
export const CHECKBOX_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CheckBoxGroupValueAccessor),
multi: true,
};
@Directive({
selector:
'checkbox-group[formControlName],checkbox-group[formControl],checkbox-group[ngModel]',
host: {
'(bindchange)': 'valueChange($event.detail.value)',
},
providers: [CHECKBOX_VALUE_ACCESSOR],
})
export class CheckBoxGroupValueAccessor
extends BuiltInControlValueAccessor
implements ControlValueAccessor
{
@ContentChildren(forwardRef(() => CheckboxControl), { descendants: true })
children!: QueryList;
valueChange(list: string[]) {
if (this.children) {
this.children.forEach((item) => {
item.updateChecked(list.some((value) => value === item.value));
});
}
this.onChange(list);
}
writeValue(list: string[]) {
if (this.children) {
this.children.forEach((item) => {
item.updateChecked(list.some((value) => value === item.value));
});
}
}
}
/**
* @description
* A `ControlValueAccessor` for writing a value and listening to changes on a checkbox input
* element.
*
* @usageNotes
*
* ### Using a checkbox with a reactive form.
*
* The following example shows how to use a checkbox with a reactive form.
*
* ```ts
* const rememberLoginControl = new FormControl();
* ```
*
* ```
*
* ```
*
* @ngModule ReactiveFormsModule
* @ngModule FormsModule
* @publicApi
*/
@Directive({
selector: 'checkbox',
host: {},
})
export class CheckboxControl {
/**
* @description
* Tracks the value of the radio input element
*/
@HostBinding('value') @Input() readonly value!: string | undefined;
@HostBinding('checked')
checked: boolean | undefined;
constructor(private elementRef: ElementRef, private renderer: Renderer2) {}
updateChecked(value: boolean) {
this.renderer.setProperty(this.elementRef.nativeElement, 'checked', value);
}
}
================================================
FILE: src/library/forms/src/directives/default_value_accessor.ts
================================================
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {
Directive,
ElementRef,
HostBinding,
Inject,
InjectionToken,
Optional,
Renderer2,
forwardRef,
} from '@angular/core';
import {
BaseControlValueAccessor,
ControlValueAccessor,
NG_VALUE_ACCESSOR,
} from './control_value_accessor';
export const DEFAULT_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => DefaultValueAccessor),
multi: true,
};
/**
* @description
* Provide this token to control if form directives buffer IME input until
* the "compositionend" event occurs.
* @publicApi
*/
export const COMPOSITION_BUFFER_MODE = new InjectionToken(
'CompositionEventMode'
);
/**
* The default `ControlValueAccessor` for writing a value and listening to changes on input
* elements. The accessor is used by the `FormControlDirective`, `FormControlName`, and
* `NgModel` directives.
*
* {@searchKeywords ngDefaultControl}
*
* @usageNotes
*
* ### Using the default value accessor
*
* The following example shows how to use an input element that activates the default value accessor
* (in this case, a text field).
*
* ```ts
* const firstNameControl = new FormControl();
* ```
*
* ```
*
* ```
*
* This value accessor is used by default for ` ` and `