├── .npmrc
├── glass-easel-miniprogram-template
├── .gitignore
├── src
│ ├── components
│ │ ├── view
│ │ │ ├── view.wxml
│ │ │ ├── view.json
│ │ │ ├── view.wxss
│ │ │ └── view.ts
│ │ └── image
│ │ │ ├── image.wxml
│ │ │ ├── image.json
│ │ │ ├── image.wxss
│ │ │ └── image.ts
│ ├── app.wxss
│ ├── app.ts
│ ├── resources
│ │ └── logo_256.png
│ ├── pages
│ │ └── index
│ │ │ ├── index.json
│ │ │ ├── index.wxss
│ │ │ ├── index.ts
│ │ │ └── index.wxml
│ └── index.html
├── typings
│ └── miniprogram.d.ts
├── webpack.dev.config.js
├── .eslintrc.js
├── tsconfig.json
├── README.md
├── package.json
└── webpack.config.js
├── .gitignore
├── glass-easel
├── .eslintignore
├── .npmignore
├── .gitignore
├── tests
│ ├── legacy
│ │ └── README.md
│ ├── tmpl
│ │ └── dev_mode.test.ts
│ ├── core
│ │ ├── data_proxy.test.ts
│ │ ├── class_list.test.ts
│ │ └── relation.test.ts
│ └── types
│ │ └── createElement.test.ts
├── jest.config.js
├── tsconfig.json
├── jest.dts.config.js
├── jest.unit.config.js
├── guide
│ └── zh_CN
│ │ ├── data_management
│ │ ├── pure_data_pattern.md
│ │ ├── property_early_init.md
│ │ ├── data_deep_copy.md
│ │ ├── data_observer.md
│ │ └── advanced_update.md
│ │ ├── advanced
│ │ ├── error_listener.md
│ │ ├── binding_map_update.md
│ │ ├── build_args.md
│ │ ├── template_engine.md
│ │ ├── component_filter.md
│ │ ├── custom_backend.md
│ │ ├── external_component.md
│ │ └── component_space.md
│ │ ├── interaction
│ │ ├── component_path.md
│ │ ├── placeholder.md
│ │ ├── behavior.md
│ │ ├── generic.md
│ │ ├── trait_behavior.md
│ │ ├── slot.md
│ │ └── template_import.md
│ │ ├── styling
│ │ ├── external_class.md
│ │ ├── style_isolation.md
│ │ └── virtual_host.md
│ │ ├── basic
│ │ ├── method.md
│ │ ├── lifetime.md
│ │ └── beginning.md
│ │ ├── tree
│ │ ├── element_iterator.md
│ │ ├── mutation_observer.md
│ │ ├── selector.md
│ │ └── node_tree_modification.md
│ │ └── index.md
├── README.md
├── src
│ ├── backend
│ │ ├── index.ts
│ │ ├── composed_backend_protocol.ts
│ │ ├── shared.ts
│ │ ├── domlike_backend_protocol.ts
│ │ └── backend_protocol.ts
│ ├── render.ts
│ ├── external_shadow_tree.ts
│ ├── type_symbol.ts
│ ├── trait_behaviors.ts
│ ├── data_utils.ts
│ ├── virtual_node.ts
│ ├── template_engine.ts
│ ├── warning.ts
│ └── dev_tools.ts
└── package.json
├── glass-easel-miniprogram-typescript
├── src
│ ├── index.ts
│ └── cli.ts
├── .gitignore
├── .eslintignore
├── tsconfig.json
├── sample-template-backend-config.d.ts
├── package.json
└── README.md
├── glass-easel-shadow-sync
├── .eslintignore
├── .gitignore
├── tests
│ ├── core
│ │ ├── event.test.ts
│ │ ├── virtual.test.ts
│ │ ├── component.test.ts
│ │ ├── placehoder.test.ts
│ │ ├── structure.test.ts
│ │ ├── binding_map.test.ts
│ │ └── slot.test.ts
│ └── spec
│ │ └── replay.test.ts
├── tsconfig.json
├── package.json
├── jest.config.js
├── src
│ ├── template_engine.ts
│ └── utils.ts
└── rollup.config.mjs
├── logo_256.png
├── glass-easel-miniprogram-adapter
├── .eslintignore
├── .npmignore
├── .gitignore
├── src
│ ├── utils.ts
│ ├── builder
│ │ └── index.ts
│ ├── index.ts
│ ├── media_query.ts
│ ├── resize.ts
│ ├── env.ts
│ └── types.ts
├── tsconfig.json
├── jest.config.js
├── tests
│ ├── base
│ │ └── env.ts
│ └── data_update.test.ts
├── README.md
├── package.json
└── rollup.config.ts
├── glass-easel-stylesheet-compiler
├── .gitignore
├── package.json
├── Cargo.toml
├── README.md
└── src
│ ├── step.rs
│ ├── output.rs
│ └── error.rs
├── glass-easel-template-compiler
├── .gitignore
├── src
│ ├── lib.rs
│ ├── path.rs
│ ├── entities.rs
│ ├── stringify
│ │ └── mod.rs
│ ├── escape.rs
│ └── binding_map.rs
├── main.js
├── tests
│ ├── stringify.rs
│ ├── group.rs
│ └── script.rs
├── module.js
├── README.md
├── Cargo.toml
├── package.json
└── cbindgen.toml
├── glass-easel-miniprogram-webpack-plugin
├── .npmignore
├── .eslintignore
├── .gitignore
├── tsconfig.json
├── helpers.ts
├── wxml_loader.js
├── package.json
└── wxss_loader.js
├── .gitattributes
├── .eslintignore
├── Cargo.toml
├── pnpm-workspace.yaml
├── .prettierrc.js
├── tsconfig.json
├── LICENSE
├── package.json
├── deprecate.js
├── logo.svg
├── .github
└── workflows
│ ├── build.yml
│ └── pages.yml
└── .eslintrc.js
/.npmrc:
--------------------------------------------------------------------------------
1 | registry= https://registry.npmjs.org/
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/.gitignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | /target
3 | /node_modules
4 | /package-lock.json
5 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/components/view/view.wxml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/glass-easel/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | /docs
4 | /coverage
5 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/app.wxss:
--------------------------------------------------------------------------------
1 | /* this is the global stylesheet */
2 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-typescript/src/index.ts:
--------------------------------------------------------------------------------
1 | export * as server from './server'
2 |
--------------------------------------------------------------------------------
/glass-easel/.npmignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /node_modules
3 | /docs
4 | /coverage
5 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/components/image/image.wxml:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | /docs
4 | /coverage
5 |
--------------------------------------------------------------------------------
/logo_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wechat-miniprogram/glass-easel/HEAD/logo_256.png
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | /docs
4 | /coverage
5 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/components/view/view.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true
3 | }
4 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-typescript/.gitignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /node_modules
3 | /dist
4 |
--------------------------------------------------------------------------------
/glass-easel/.gitignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /node_modules
3 | /dist
4 | /docs
5 | /coverage
6 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/components/image/image.json:
--------------------------------------------------------------------------------
1 | {
2 | "component": true
3 | }
4 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/components/view/view.wxss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-typescript/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | /docs
4 | /coverage
5 |
--------------------------------------------------------------------------------
/glass-easel-stylesheet-compiler/.gitignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /pkg
3 | /pkg-nodejs
4 | /pkg-web
5 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/.gitignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /pkg
3 | /pkg-nodejs
4 | /pkg-web
5 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/.npmignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /node_modules
3 | /docs
4 | /coverage
5 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/components/image/image.wxss:
--------------------------------------------------------------------------------
1 | :host {
2 | display: block;
3 | }
4 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/.gitignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /node_modules
3 | /dist
4 | /docs
5 | /coverage
6 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/.gitignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /node_modules
3 | /dist
4 | /docs
5 | /coverage
6 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-webpack-plugin/.npmignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /node_modules
3 | /docs
4 | /coverage
5 |
--------------------------------------------------------------------------------
/glass-easel/tests/legacy/README.md:
--------------------------------------------------------------------------------
1 | # Legacy tests
2 |
3 | These tests are imported from the legacy framework.
4 |
--------------------------------------------------------------------------------
/glass-easel/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | projects: ['./jest.unit.config.js', './jest.dts.config.js'],
3 | }
4 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-webpack-plugin/.eslintignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /dist
3 | /docs
4 | /coverage
5 | /index.js
6 | /helpers.js
7 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | * text=auto
2 |
3 | *.md text eol=lf
4 | *.js text eol=lf
5 | *.mjs text eol=lf
6 | *.ts text eol=lf
7 | *.json text eol=lf
8 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /target
2 | /node_modules
3 | /glass-easel-miniprogram-template
4 | /glass-easel-template-compiler
5 | /glass-easel-stylesheet-compiler
6 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-webpack-plugin/.gitignore:
--------------------------------------------------------------------------------
1 | /package-lock.json
2 | /node_modules
3 | /dist
4 | /docs
5 | /coverage
6 | /index.js
7 | /helpers.js
8 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/components/image/image.ts:
--------------------------------------------------------------------------------
1 | export default Component({
2 | properties: {
3 | src: String,
4 | },
5 | data: {},
6 | })
7 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/app.ts:
--------------------------------------------------------------------------------
1 | // this file is always executed on startup
2 |
3 | // eslint-disable-next-line no-console
4 | console.info('App started')
5 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/src/utils.ts:
--------------------------------------------------------------------------------
1 | export const guid = (): string =>
2 | Math.floor((1 + Math.random()) * 0x100000000)
3 | .toString(16)
4 | .slice(1)
5 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/resources/logo_256.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wechat-miniprogram/glass-easel/HEAD/glass-easel-miniprogram-template/src/resources/logo_256.png
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/pages/index/index.json:
--------------------------------------------------------------------------------
1 | {
2 | "usingComponents": {
3 | "view": "/components/view/view",
4 | "image": "/components/image/image"
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/pages/index/index.wxss:
--------------------------------------------------------------------------------
1 | .logo {
2 | width: 256px;
3 | margin: 0 auto;
4 | }
5 |
6 | .hello {
7 | margin: 20px;
8 | text-align: center;
9 | }
10 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "glass-easel-template-compiler",
4 | "glass-easel-stylesheet-compiler",
5 | ]
6 | resolver = "2"
7 |
8 | [profile.release]
9 | opt-level = 3
10 | lto = true
11 |
--------------------------------------------------------------------------------
/glass-easel/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": [
4 | "*.ts",
5 | "src/**/*.ts",
6 | "tests/**/*.ts",
7 | "examples/**/*.ts"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-webpack-plugin/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "module": "CommonJS"
5 | },
6 | "include": [
7 | "*.ts"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/pages/index/index.ts:
--------------------------------------------------------------------------------
1 | export default Page({
2 | data: {
3 | showAgain: false,
4 | },
5 | helloTap() {
6 | this.setData({
7 | showAgain: true,
8 | })
9 | },
10 | })
11 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "include": [
4 | "*.ts",
5 | "src/**/*.ts",
6 | "tests/**/*.ts",
7 | "examples/**/*.ts"
8 | ]
9 | }
10 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/typings/miniprogram.d.ts:
--------------------------------------------------------------------------------
1 | import type { PageConstructor, ComponentConstructor } from 'glass-easel-miniprogram-adapter'
2 |
3 | declare global {
4 | const Page: PageConstructor
5 | const Component: ComponentConstructor
6 | }
7 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-typescript/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "module": "CommonJS",
5 | "outDir": "dist"
6 | },
7 | "include": [
8 | "src/*.ts"
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/glass-easel/jest.dts.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | displayName: {
3 | color: 'blue',
4 | name: 'types',
5 | },
6 | roots: ['tests'],
7 | runner: 'jest-runner-tsd',
8 | testMatch: ['**/types/**/?(*.)+(spec|test).[jt]s?(x)'],
9 | }
10 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-webpack-plugin/helpers.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | export const escapeJsString = (str: string) =>
4 | str.replace(/["'\\\r\n`]/g, (c) => {
5 | if (c === '\r') return '\\r'
6 | if (c === '\n') return '\\n'
7 | return `\\${c}`
8 | })
9 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tests/core/event.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import * as coreEnv from 'glass-easel/tests/base/env'
3 | import { shadowSyncBackend } from '../base/env'
4 |
5 | ;(coreEnv as any).shadowBackend = shadowSyncBackend
6 |
7 | require('../../../glass-easel/tests/tmpl/event.test')
8 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tests/core/virtual.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import * as coreEnv from 'glass-easel/tests/base/env'
3 | import { shadowSyncBackend } from '../base/env'
4 |
5 | ;(coreEnv as any).shadowBackend = shadowSyncBackend
6 |
7 | require('../../../glass-easel/tests/legacy/virtual.test')
8 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tests/core/component.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import * as coreEnv from 'glass-easel/tests/base/env'
3 | import { shadowSyncBackend } from '../base/env'
4 |
5 | ;(coreEnv as any).shadowBackend = shadowSyncBackend
6 |
7 | require('../../../glass-easel/tests/legacy/component.test')
8 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tests/core/placehoder.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import * as coreEnv from 'glass-easel/tests/base/env'
3 | import { shadowSyncBackend } from '../base/env'
4 |
5 | ;(coreEnv as any).shadowBackend = shadowSyncBackend
6 |
7 | require('../../../glass-easel/tests/core/placeholder.test')
8 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tests/core/structure.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import * as coreEnv from 'glass-easel/tests/base/env'
3 | import { shadowSyncBackend } from '../base/env'
4 |
5 | ;(coreEnv as any).shadowBackend = shadowSyncBackend
6 |
7 | require('../../../glass-easel/tests/tmpl/structure.test')
8 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tests/core/binding_map.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import * as coreEnv from 'glass-easel/tests/base/env'
3 | import { shadowSyncBackend } from '../base/env'
4 |
5 | ;(coreEnv as any).shadowBackend = shadowSyncBackend
6 |
7 | require('../../../glass-easel/tests/tmpl/binding_map.test')
8 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/webpack.dev.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 |
3 | const config = require('./webpack.config')
4 |
5 | config[0].mode = 'development'
6 | config[0].resolve.alias['glass-easel'] = 'glass-easel/dist/glass_easel.dev.all.es.js'
7 |
8 | module.exports = config
9 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/pages/index/index.wxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Hello world!
6 |
7 |
8 | Hello world again!
9 |
10 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig.json",
3 | "compilerOptions": {
4 | "paths": {
5 | "glass-easel": ["../glass-easel/src"]
6 | }
7 | },
8 | "include": [
9 | "src/**/*.ts",
10 | "tests/**/*.ts",
11 | "examples/**/*.ts"
12 | ]
13 | }
14 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/src/builder/index.ts:
--------------------------------------------------------------------------------
1 | export { BehaviorBuilder } from './behavior_builder'
2 | export type { DefaultBehaviorBuilder } from './behavior_builder'
3 | export { ComponentBuilder } from './component_builder'
4 | export type { DefaultComponentBuilder } from './component_builder'
5 | export { BuilderContext } from './type_utils'
6 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - "glass-easel"
3 | - "glass-easel-miniprogram-adapter"
4 | - "glass-easel-miniprogram-webpack-plugin"
5 | - "glass-easel-shadow-sync"
6 | - "glass-easel-miniprogram-template"
7 | - "glass-easel-miniprogram-typescript"
8 | - "glass-easel-template-compiler"
9 | - "glass-easel-stylesheet-compiler"
10 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parserOptions: {
4 | ecmaVersion: 9,
5 | sourceType: 'module',
6 | },
7 | parser: '@typescript-eslint/parser',
8 | plugins: ['@typescript-eslint', 'import', 'promise'],
9 | globals: {
10 | Component: true,
11 | Page: true,
12 | },
13 | }
14 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tests/core/slot.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 | import * as coreEnv from 'glass-easel/tests/base/env'
3 | import { shadowSyncBackend } from '../base/env'
4 |
5 | ;(coreEnv as any).shadowBackend = shadowSyncBackend
6 |
7 | require('../../../glass-easel/tests/legacy/slot.test')
8 | require('../../../glass-easel/tests/core/slot.test')
9 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/src/index.ts:
--------------------------------------------------------------------------------
1 | export * as glassEasel from 'glass-easel'
2 | export { AssociatedBackend, Root } from './backend'
3 | export * as component from './component'
4 | export * as behavior from './behavior'
5 | export * as builder from './builder'
6 | export { MiniProgramEnv } from './env'
7 | export * from './space'
8 | export * as types from './types'
9 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest/presets/js-with-babel',
3 | transform: {
4 | '^.+\\.ts$': [
5 | 'ts-jest',
6 | {
7 | tsconfig: 'tsconfig.json',
8 | },
9 | ],
10 | },
11 | roots: ['tests'],
12 | testEnvironment: 'jsdom',
13 | collectCoverageFrom: ['src/**/*.ts'],
14 | }
15 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strict": true,
4 | "module": "es6",
5 | "target": "es5",
6 | "esModuleInterop": true,
7 | "moduleResolution": "node",
8 | "lib": ["ES6", "ES7", "DOM", "ESNext"]
9 | },
10 | "include": [
11 | "src/**/*.ts",
12 | "typings"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-typescript/sample-template-backend-config.d.ts:
--------------------------------------------------------------------------------
1 | export type GlassEaselTemplateBackendConfig = {
2 | name: 'sample'
3 | description: 'An example of template backend configuration.'
4 | majorVersion: 1
5 | minorVersion: 0
6 | }
7 |
8 | export type ComponentProperties = {
9 | div: {
10 | hidden: boolean
11 | }
12 | img: {
13 | src: string
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/.prettierrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | printWidth: 100,
3 | tabWidth: 2,
4 | useTabs: false,
5 | semi: false,
6 | singleQuote: true,
7 | quoteProps: 'as-needed',
8 | trailingComma: 'all',
9 | bracketSpacing: true,
10 | arrowParens: 'always',
11 | requirePragma: false,
12 | insertPragma: false,
13 | proseWrap: 'preserve',
14 | endOfLine: 'lf',
15 | embeddedLanguageFormatting: 'auto',
16 | }
17 |
--------------------------------------------------------------------------------
/glass-easel/jest.unit.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest/presets/js-with-babel',
3 | transform: {
4 | '^.+\\.ts$': [
5 | 'ts-jest',
6 | {
7 | tsconfig: 'tsconfig.json',
8 | },
9 | ],
10 | },
11 | roots: ['tests'],
12 | testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)', '!**/types/**'],
13 | testEnvironment: 'jsdom',
14 | collectCoverageFrom: ['src/**/*.ts'],
15 | }
16 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glass-easel-shadow-sync",
3 | "version": "0.17.2",
4 | "main": "dist/glass-easel-shadow-sync.js",
5 | "scripts": {
6 | "build": "rollup -c rollup.config.mjs",
7 | "test": "jest -c jest.config.js"
8 | },
9 | "dependencies": {
10 | "glass-easel": "workspace:*"
11 | },
12 | "devDependencies": {
13 | "glass-easel-template-compiler": "workspace:*"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/src/lib.rs:
--------------------------------------------------------------------------------
1 | #![allow(clippy::needless_borrow)]
2 |
3 | #[allow(unused_imports)]
4 | #[macro_use]
5 | extern crate log;
6 | #[macro_use]
7 | extern crate lazy_static;
8 |
9 | mod binding_map;
10 | mod group;
11 | pub mod parse;
12 | pub mod stringify;
13 | pub use group::*;
14 | #[cfg(feature = "c_bindings")]
15 | pub mod cbinding;
16 | mod entities;
17 | mod escape;
18 | mod js_bindings;
19 | mod path;
20 | mod proc_gen;
21 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/jest.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | preset: 'ts-jest/presets/js-with-babel',
3 | transform: {
4 | '^.+\\.ts$': [
5 | 'ts-jest',
6 | {
7 | tsconfig: 'tsconfig.json',
8 | },
9 | ],
10 | },
11 | moduleNameMapper: {
12 | '^glass-easel$': '/../glass-easel/src',
13 | },
14 | roots: ['tests'],
15 | testMatch: ['**/?(*.)+(spec|test).[jt]s?(x)', '!**/types/**'],
16 | testEnvironment: 'jsdom',
17 | collectCoverageFrom: ['src/**/*.ts'],
18 | }
19 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/README.md:
--------------------------------------------------------------------------------
1 | # glass-easel-miniprogram-template
2 |
3 | The template mini-program code for the glass-easel project.
4 |
5 | Refer to the [glass-easel](https://github.com/wechat-miniprogram/glass-easel) project for further details.
6 |
7 | ## Build
8 |
9 | `nodejs` toolchain should be globally installed.
10 |
11 | Install dependencies:
12 |
13 | ```sh
14 | npm install
15 | ```
16 |
17 | Build:
18 |
19 | ```sh
20 | npm run build
21 | ```
22 |
23 | Then open `dist/index.html` in the browser.
24 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-webpack-plugin/wxml_loader.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const { escapeJsString } = require('./helpers')
3 |
4 | module.exports = function (src, prevMap, meta) {
5 | const { addTemplate } = this.query
6 | const { compPath, deps, codeRoot } = addTemplate(src, this.currentModule)
7 | const requires = deps.map((x) => {
8 | const p = path.join(codeRoot, `${x}.wxml`)
9 | return `require('${escapeJsString(p)}');`
10 | })
11 | return `
12 | ${requires.join('')}
13 | module.exports = '${escapeJsString(compPath)}'
14 | `
15 | }
16 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/data_management/pure_data_pattern.md:
--------------------------------------------------------------------------------
1 | # 纯数据字段
2 |
3 | 通过组件选项 `pureDataPattern` 可以使得一些数据字段不能用于模板上。这个选项接受一个正则表达式,符合这个正则表达式的字段名不能用于模板。
4 |
5 | ```js
6 | export const myComponent = componentSpace.defineComponent({
7 | options: {
8 | // 所有以 _ 开头的字段都不能用于模板
9 | pureDataPattern: /^_/,
10 | },
11 | // 模板上的纯数据字段将视为 undefined
12 | template: compileTemplate(`
13 | {{ _a }}
14 | `),
15 | data: {
16 | _a: 1,
17 | },
18 | })
19 | ```
20 |
21 | 使用 `pureDataPattern` 通常不会带来性能方面的影响。但如果通过 `dataDeepCopy` 禁用数据拷贝, `pureDataPattern` 可能仍然会带来部分数据拷贝开销。
22 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/main.js:
--------------------------------------------------------------------------------
1 | const path = require('node:path')
2 | const fs = require('node:fs')
3 | const { fileURLToPath } = require('node:url')
4 | const bg = require('./pkg/glass_easel_template_compiler_bg.cjs.js')
5 |
6 | const bytes = fs.readFileSync(
7 | path.resolve(__dirname, './pkg/glass_easel_template_compiler_bg.wasm'),
8 | )
9 |
10 | const wasmModule = new WebAssembly.Module(bytes)
11 | const wasmInstance = new WebAssembly.Instance(wasmModule, {
12 | './glass_easel_template_compiler_bg.js': bg,
13 | })
14 |
15 | bg.__wbg_set_wasm(wasmInstance.exports)
16 |
17 | module.exports = bg
18 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/tests/stringify.rs:
--------------------------------------------------------------------------------
1 | use glass_easel_template_compiler::{stringify::Stringifier, TmplGroup};
2 |
3 | #[test]
4 | fn stringifier() {
5 | const SRC_A: &str = r#" Hello world!
"#;
6 | let mut group = TmplGroup::new();
7 | group.add_tmpl("a", SRC_A);
8 | let tmpl = group.get_tree("a").unwrap();
9 | let mut out = String::new();
10 | Stringifier::new(&mut out, "a", Some(SRC_A), Default::default())
11 | .run(tmpl)
12 | .unwrap();
13 | assert_eq!(
14 | out,
15 | "\n\n Hello world! \n
\n"
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/glass-easel/README.md:
--------------------------------------------------------------------------------
1 | # glass-easel
2 |
3 | This is the core module of the glass-easel project.
4 |
5 | Refer to the [glass-easel](https://github.com/wechat-miniprogram/glass-easel) project for further details.
6 |
7 | ## Build
8 |
9 | `nodejs` toolchain should be globally installed.
10 |
11 | Install dependencies and prepare environment:
12 |
13 | ```sh
14 | npm install
15 | npm run init-dev
16 | ```
17 |
18 | Build:
19 |
20 | ```sh
21 | npm run build
22 | ```
23 |
24 | Build Doc:
25 |
26 | ```sh
27 | npm run doc
28 | ```
29 |
30 | Test:
31 |
32 | ```sh
33 | npm test
34 | ```
35 |
36 | Test coverage:
37 |
38 | ```sh
39 | npm run coverage
40 | ```
41 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/src/components/view/view.ts:
--------------------------------------------------------------------------------
1 | import { StyleSegmentIndex } from 'glass-easel'
2 |
3 | export default Component({
4 | properties: {
5 | hidden: Boolean,
6 | },
7 | data: {},
8 | observers: {
9 | hidden(hidden: boolean) {
10 | // `this._$` is the underlying glass-easel element
11 | // (this cannot be retrieved in MiniProgram environment)
12 | const glassEaselElement = this._$
13 | if (hidden) {
14 | glassEaselElement.setNodeStyle('display: none', StyleSegmentIndex.TEMP_EXTRA)
15 | } else {
16 | glassEaselElement.setNodeStyle('', StyleSegmentIndex.TEMP_EXTRA)
17 | }
18 | },
19 | },
20 | })
21 |
--------------------------------------------------------------------------------
/glass-easel/src/backend/index.ts:
--------------------------------------------------------------------------------
1 | import type * as backend from './backend_protocol'
2 | import type * as composedBackend from './composed_backend_protocol'
3 | import type * as domlikeBackend from './domlike_backend_protocol'
4 |
5 | export * from './shared'
6 | export * as backend from './backend_protocol'
7 | export * as composedBackend from './composed_backend_protocol'
8 | export * as domlikeBackend from './domlike_backend_protocol'
9 |
10 | export type GeneralBackendContext =
11 | | backend.Context
12 | | composedBackend.Context
13 | | domlikeBackend.Context
14 |
15 | export type GeneralBackendElement =
16 | | backend.Element
17 | | composedBackend.Element
18 | | domlikeBackend.Element
19 |
--------------------------------------------------------------------------------
/glass-easel/src/render.ts:
--------------------------------------------------------------------------------
1 | import { type GeneralBackendContext } from './backend'
2 | import { type Element } from './element'
3 | import { safeCallback } from './func_arr'
4 |
5 | const triggerRenderOnContext = (
6 | context: GeneralBackendContext,
7 | cb: ((err: Error | null) => void) | null,
8 | ) => {
9 | context.render(() => {
10 | if (typeof cb === 'function') {
11 | safeCallback('render', cb, context, [null])
12 | }
13 | })
14 | }
15 |
16 | export const triggerRender = (element: Element, callback?: (err: Error | null) => void) => {
17 | const context = element.getBackendContext()
18 | if (context) {
19 | triggerRenderOnContext(context, callback || null)
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/module.js:
--------------------------------------------------------------------------------
1 | import path from 'node:path'
2 | import fs from 'node:fs'
3 | import { fileURLToPath } from 'node:url'
4 | import * as bg from './pkg/glass_easel_template_compiler_bg.js'
5 |
6 | const __filename = fileURLToPath(import.meta.url)
7 | const __dirname = path.dirname(__filename)
8 |
9 | const bytes = fs.readFileSync(
10 | path.resolve(__dirname, './pkg/glass_easel_template_compiler_bg.wasm'),
11 | )
12 |
13 | const wasmModule = new WebAssembly.Module(bytes)
14 | const wasmInstance = new WebAssembly.Instance(wasmModule, {
15 | './glass_easel_template_compiler_bg.js': bg,
16 | })
17 |
18 | bg.__wbg_set_wasm(wasmInstance.exports)
19 |
20 | export * from './pkg/glass_easel_template_compiler_bg.js'
21 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/data_management/property_early_init.md:
--------------------------------------------------------------------------------
1 | # 组件初始化策略
2 |
3 | 组件实例初始化流程有两种:
4 |
5 | 1. 总是以自己定义的数据来初始化模板、触发 created 生命周期,然后如有必要,应用外部设置的组件属性、触发数据监听器、再更新一次模板;
6 | 2. 先在自己定义的数据的基础上,应用外部设置的组件属性、触发数据监听器,然后初始化模板、触发 created 生命周期。
7 |
8 | 前者的好处是数据监听器永远在 created 生命周期之后触发,逻辑时序比较稳定;坏处是常常需要一次额外的模板更新,性能相对较差。
9 |
10 | glass-easel 目前以前者为默认的初始化方式,通过设置 `propertyEarlyInit` 选项可以改为后者:
11 |
12 | ```js
13 | export const childComponent = componentSpace.defineComponent({
14 | options: {
15 | propertyEarlyInit: true,
16 | },
17 | data: {
18 | a: 1,
19 | },
20 | observers: {
21 | a() {
22 | // 可能早于 created 生命周期触发
23 | }
24 | },
25 | lifetimes: {
26 | created() {
27 | // 可能晚于 observers 触发
28 | },
29 | },
30 | })
31 | ```
32 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/README.md:
--------------------------------------------------------------------------------
1 | # glass-easel-template-compiler
2 |
3 | The template compiler for the glass-easel project.
4 |
5 | Refer to the [glass-easel](https://github.com/wechat-miniprogram/glass-easel) project for further details.
6 |
7 | ## Build
8 |
9 | `rust` toolchain and `wasm-pack` should be globally installed.
10 |
11 | Build WebAssembly binary:
12 |
13 | ```sh
14 | wasm-pack build glass-easel-template-compiler --target nodejs --out-dir pkg-nodejs
15 | ```
16 |
17 | Build binary:
18 |
19 | ```sh
20 | cargo build --release
21 | ```
22 |
23 | Build for simple browser usage:
24 |
25 | ```sh
26 | wasm-pack build glass-easel-template-compiler --target no-modules --out-dir pkg-web
27 | ```
28 |
29 | See the main project for the detailed usage.
30 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/advanced/error_listener.md:
--------------------------------------------------------------------------------
1 | # 警告与错误
2 |
3 | # 错误监听器
4 |
5 | 在组件生命周期、事件回调中抛出的异常,会被 glass-easel 捕获。想要获取这些异常,可以注册一个错误监听器:
6 |
7 | ```js
8 | glassEasel.addGlobalErrorListener((err, { element }) => {
9 | err // 捕获的异常对象
10 | if element instanceof glassEasel.Comonent {
11 | element // 与这个异常相关的组件
12 | }
13 | // 返回 false 将取消 console 输出
14 | return false
15 | })
16 | ```
17 |
18 | 如果没有监听器或者监听器都没返回 `false` ,则错误信息会被输出到 console 中。如果 `glassEasel.globalOptions.throwGlobalError` 被设为 `true` ,则它会被再次抛出到全局。
19 |
20 | # 警告监听器
21 |
22 | glass-easel 也会产生一些警告用于提示一些常见的误用情况。默认情况下,警告会输出到 console 中。
23 |
24 | 类似于错误,警告也可以被监听到,例如:
25 |
26 | ```js
27 | glassEasel.addGlobalWarningListener((message) => {
28 | message // 警告信息
29 | // 返回 false 将取消 console 输出
30 | return false
31 | })
32 | ```
33 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/tests/base/env.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-new-func */
2 | /* eslint-disable @typescript-eslint/no-implied-eval */
3 |
4 | import type * as glassEasel from 'glass-easel'
5 | import { TmplGroup } from 'glass-easel-template-compiler'
6 |
7 | export const tmpl = (src: string): glassEasel.template.ComponentTemplate => {
8 | const group = new TmplGroup()
9 | group.addTmpl('', src)
10 | const genObjectSrc = `return ${group.getTmplGenObjectGroups()}`
11 | group.free()
12 | // console.info(genObjectSrc)
13 | const genObjectGroupList = new Function(genObjectSrc)() as { [key: string]: any }
14 | return {
15 | groupList: genObjectGroupList,
16 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
17 | content: genObjectGroupList['']!,
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/interaction/component_path.md:
--------------------------------------------------------------------------------
1 | # 组件路径
2 |
3 | 可以给组件起一个路径名称,用于给组件归类。
4 |
5 | 使用 Definition API 给组件命名的示例:
6 |
7 | ```js
8 | export const myComponent = componentSpace.defineComponent({
9 | is: 'path/to/my/component',
10 | })
11 | ```
12 |
13 | 使用 Chaining API 给组件命名的示例:
14 |
15 | ```js
16 | export const myComponent = componentSpace.define('path/to/my/component')
17 | .registerComponent()
18 | ```
19 |
20 | 在引用其他组件时,可以使用组件路径来指定,这对于含有递归引用的组件很实用:
21 |
22 | ```js
23 | componentSpace.defineComponent({
24 | is: 'path/to/one/component',
25 | using: {
26 | // 使用相对路径来指定另一个组件
27 | another: '../another/component',
28 | },
29 | })
30 | componentSpace.defineComponent({
31 | is: 'path/to/another/component',
32 | using: {
33 | // 使用绝对路径来指定另一个组件
34 | one: '/path/to/one/component',
35 | },
36 | })
37 | ```
38 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/src/media_query.ts:
--------------------------------------------------------------------------------
1 | import type * as glassEasel from 'glass-easel'
2 | import { type GeneralComponent } from './component'
3 |
4 | export class MediaQueryObserver {
5 | private _$comp: GeneralComponent
6 | private _$observer: glassEasel.backend.Observer | null = null
7 |
8 | /** @internal */
9 | constructor(comp: GeneralComponent) {
10 | this._$comp = comp
11 | }
12 |
13 | observe(
14 | descriptor: glassEasel.backend.MediaQueryStatus,
15 | listener: (status: { matches: boolean }) => void,
16 | ) {
17 | if (this._$observer) this._$observer.disconnect()
18 | this._$observer =
19 | this._$comp._$.getBackendContext()?.createMediaQueryObserver?.(descriptor, listener) || null
20 | }
21 |
22 | disconnect() {
23 | if (this._$observer) this._$observer.disconnect()
24 | this._$observer = null
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/glass-easel/src/external_shadow_tree.ts:
--------------------------------------------------------------------------------
1 | import { type Event, type ShadowedEvent } from './event'
2 | import { type GeneralBackendElement } from './backend'
3 |
4 | /**
5 | * An external shadow root
6 | *
7 | * It can be used to build an external component.
8 | * External component is a customizable subtree that can be composed with normal components.
9 | * It allows third-party frameworks to render a subtree and then compose it together.
10 | * However, the subtree must be created in the same backend context.
11 | */
12 | export interface ExternalShadowRoot {
13 | root: GeneralBackendElement
14 | slot: GeneralBackendElement
15 | getIdMap(): { [id: string]: GeneralBackendElement }
16 | handleEvent(target: GeneralBackendElement, event: Event): void
17 | setListener(
18 | elem: GeneralBackendElement,
19 | ev: string,
20 | listener: (event: ShadowedEvent) => unknown,
21 | ): void
22 | }
23 |
--------------------------------------------------------------------------------
/glass-easel-stylesheet-compiler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glass-easel-stylesheet-compiler",
3 | "description": "The stylesheet compiler of the glass-easel project.",
4 | "version": "0.17.2",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/wechat-miniprogram/glass-easel.git"
8 | },
9 | "keywords": ["glass-easel"],
10 | "author": "wechat-miniprogram",
11 | "license": "MIT",
12 | "bugs": {
13 | "url": "https://github.com/wechat-miniprogram/glass-easel/issues"
14 | },
15 | "homepage": "https://github.com/wechat-miniprogram/glass-easel",
16 | "files": [
17 | "pkg-nodejs/glass_easel_stylesheet_compiler*",
18 | "/README.md"
19 | ],
20 | "main": "pkg-nodejs/glass_easel_stylesheet_compiler.js",
21 | "types": "pkg-nodejs/glass_easel_stylesheet_compiler.d.ts",
22 | "scripts": {
23 | "build": "wasm-pack build --target nodejs --out-dir pkg-nodejs"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "strictNullChecks": true,
4 | "strictPropertyInitialization": true,
5 | "noUncheckedIndexedAccess": true,
6 | "useUnknownInCatchVariables": true,
7 | "noImplicitAny": true,
8 | "noImplicitOverride": true,
9 | "noImplicitReturns": true,
10 | "noImplicitThis": true,
11 | "strictFunctionTypes": true,
12 | "strictBindCallApply": true,
13 | "module": "es6",
14 | "target": "es6",
15 | "esModuleInterop": true,
16 | "moduleResolution": "node",
17 | "lib": ["ES6", "ES7", "DOM", "ESNext"],
18 | "types": ["jest", "node"],
19 | "declaration": false,
20 | "preserveConstEnums": true,
21 | "stripInternal": true
22 | },
23 | "ts-node": {
24 | "compilerOptions": {
25 | "module": "CommonJS"
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/advanced/binding_map_update.md:
--------------------------------------------------------------------------------
1 | # 绑定映射表更新
2 |
3 | glass-easel 内置的模板引擎支持两种更新方式。
4 |
5 | * 虚拟树更新:更新时,遍历需要更新的 Shadow Tree ,并更新变化了的数据绑定;
6 | * 绑定映射表更新:更新时,根据改变的数据字段名、找到用到了该字段数据绑定表达式,然后更新它。
7 |
8 | 绑定映射表更新通常是一种更快速高效的更新方式,但它不能用于更新 `wx:if` `wx:for` 子节点树内部用到的数据字段。
9 |
10 | 每次更新时, glass-easel 会根据被设置的数据字段名,来选择应该使用哪种更新方式。
11 |
12 | 如果想要指定只其中一种更新方式,可以在编译模板时控制:
13 |
14 | ```js
15 | const compileTemplate = (src: string, updateMode = '') => {
16 | const group = new TmplGroup()
17 | group.addTmpl('', src)
18 | const genObjectSrc = `return ${group.getTmplGenObjectGroups()}`
19 | group.free()
20 | const genObjectGroupList = (new Function(genObjectSrc))() as { [key: string]: any }
21 | return {
22 | content: genObjectGroupList[''],
23 | updateMode,
24 | }
25 | }
26 | // 指定模板只使用虚拟树更新
27 | compileTemplate('', 'virtualTree')
28 | // 指定模板只使用绑定映射表更新
29 | compileTemplate('', 'bindingMap')
30 | ```
31 |
32 | 注意:在不支持的模板上强行只使用绑定映射表更新,会导致出错或更新异常。
33 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/interaction/placeholder.md:
--------------------------------------------------------------------------------
1 | # 占位组件
2 |
3 | 有时,出于性能考虑,一些组件需要被延迟加载。此时,可以设置一个占位组件,当一个组件缺失时,可以用占位组件来暂时代替。等到组件被注册之后,占位组件会被自动替换成它。例如:
4 |
5 | ```js
6 | export const placeholderComponent = componentSpace.defineComponent({
7 | template: compileTemplate(`
8 | Loading...
9 | `),
10 | })
11 |
12 | // 使用 Definition API 设置占位组件
13 | export const myComponent = componentSpace.defineComponent({
14 | using: {
15 | child: 'lazy-components/child',
16 | placeholder: placeholderComponent,
17 | },
18 | placeholders: {
19 | child: 'placeholder',
20 | },
21 | })
22 | // 或使用 Chaining API 设置占位组件
23 | export const myComponent = componentSpace.define()
24 | .usingComponents({
25 | child: 'lazy-components/child',
26 | placeholder: placeholderComponent,
27 | })
28 | .placeholders({
29 | child: 'placeholder',
30 | })
31 | .registerComponent()
32 | ```
33 |
34 | 等到对应的被注册时,占位组件会被自动替换:
35 |
36 | ```js
37 | componentSpace.defineComponent({
38 | is: 'lazy-components/child',
39 | })
40 | ```
41 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2023 wechat-miniprogram
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
8 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glass-easel-miniprogram-template",
3 | "version": "0.17.2",
4 | "main": "src/index.ts",
5 | "scripts": {
6 | "build": "webpack --config webpack.config.js",
7 | "dev": "webpack --config webpack.dev.config.js --watch"
8 | },
9 | "dependencies": {
10 | "glass-easel": "workspace:*",
11 | "glass-easel-miniprogram-adapter": "workspace:*"
12 | },
13 | "devDependencies": {
14 | "@typescript-eslint/eslint-plugin": "^6.21.0",
15 | "@typescript-eslint/parser": "^6.21.0",
16 | "css-loader": "^6.7.1",
17 | "eslint": "^7.32.0",
18 | "eslint-plugin-import": "^2.22.1",
19 | "eslint-plugin-promise": "^4.2.1",
20 | "glass-easel-miniprogram-typescript": "workspace:*",
21 | "glass-easel-miniprogram-webpack-plugin": "workspace:*",
22 | "less": "^4.1.3",
23 | "less-loader": "^11.0.0",
24 | "mini-css-extract-plugin": "^2.6.1",
25 | "ts-loader": "^9.4.2",
26 | "typescript": "~5.2.2",
27 | "webpack": "^5.101.3",
28 | "webpack-cli": "^6.0.1"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/glass-easel-stylesheet-compiler/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "glass-easel-stylesheet-compiler"
3 | version = "0.17.2"
4 | authors = ["LastLeaf "]
5 | description = "The stylesheet compiler of the glass-easel project."
6 | license = "MIT"
7 | documentation = "https://github.com/wechat-miniprogram/glass-easel"
8 | repository = "https://github.com/wechat-miniprogram/glass-easel"
9 | homepage = "https://github.com/wechat-miniprogram/glass-easel"
10 | edition = "2021"
11 |
12 | [lib]
13 | crate-type = ["cdylib", "rlib"]
14 |
15 | [[bin]]
16 | name = "glass-easel-stylesheet-compiler"
17 | path = "src/main.rs"
18 |
19 | [features]
20 | default = ["js_bindings"]
21 | js_bindings = []
22 |
23 | [dependencies]
24 | cssparser = "0.34"
25 | sourcemap = "6"
26 | serde = "1"
27 | serde_json = "1"
28 | serde-wasm-bindgen = "0.6"
29 | wasm-bindgen = "0.2"
30 | js-sys = "0.3"
31 | log = "0.4"
32 | console_log = "0.2"
33 | env_logger = "0.9"
34 | clap = { version = "4", features = ["derive"] }
35 | urlencoding = "2"
36 |
37 | [package.metadata.wasm-pack.profile.release]
38 | wasm-opt = false
39 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/advanced/build_args.md:
--------------------------------------------------------------------------------
1 | # 构建参数
2 |
3 | glass-easel 的构建脚本会根据 `GLASS_EASEL_ARGS` 环境变量来构建出不同的结果。
4 |
5 | 首先,构建模式会决定构建产物的文件名(位于 dist 目录下)以及不同的 [自定义后端](custom_backend.md) 支持度。目前支持的构建模式如下。
6 |
7 | | 构建模式 | 文件名 | 支持的自定义后端 | 产物使用方式 |
8 | | -------- | ------ | ---------------- | ------------ |
9 | | all | glass_easel.all | 支持全部后端(运行时自动选择) | CommonJS |
10 | | shadow | glass_easel.shadow | 支持 shadow 模式后端 | CommonJS |
11 | | shadow-global | glass_easel.shadow.global | 支持 shadow 模式后端 | 全局变量 |
12 | | composed | glass_easel.composed | 支持 composed 模式后端 | CommonJS |
13 | | composed-global | glass_easel.composed.global | 支持 composed 模式后端 | 全局变量 |
14 | | domlike | glass_easel.domlike | 支持 DOM 后端 | CommonJS |
15 | | domlike-global | glass_easel.domlike.global | 支持 DOM 后端 | 全局变量 |
16 |
17 | 一次可以同时启用多个构建模式。在 node.js 和打包工具中,第一个构建模式会视为被 import 时采用的构建结果。
18 |
19 | 额外参数可以修饰构建结果:
20 |
21 | * `--minimize` / `--no-minimize` 可以启用、禁用代码压缩混淆;
22 | * `--dev` 可以激活 development 构建模式。
23 |
24 | 例如,如果想要构建出支持全部后端和全局量模式 DOM 后端的两种构建结果、同时禁用代码压缩混淆,则 `GLASS_EASEL_ARGS` 环境变量应设为:
25 |
26 | ```
27 | all domlike-global --no-minimize
28 | ```
29 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/styling/external_class.md:
--------------------------------------------------------------------------------
1 | # 外部样式类
2 |
3 | 有时,一个组件的部分样式规则需要由它的使用者来指定。此时可以使用 **外部样式类** 。
4 |
5 | 组件可以指定它自身的一些 class 可以由组件外传递,例如:
6 |
7 | ```js
8 | // 使用 Definition API 指定外部样式类
9 | export const childComponent = componentSpace.defineComponent({
10 | externalClasses: ['my-class'],
11 | // 在模板的 class 中可以使用外部样式类
12 | template: compileTemplate(`
13 |
14 | `),
15 | })
16 | // 或使用 Chaining API 指定外部样式类
17 | export const childComponent = componentSpace.define()
18 | .externalClasses(['my-class'])
19 | .template(compileTemplate(`
20 |
21 | `))
22 | .registerComponent()
23 | ```
24 |
25 | 使用这个组件时,就可以指定:
26 |
27 | ```js
28 | export const myComponent = componentSpace.defineComponent({
29 | using: {
30 | child: childComponent,
31 | },
32 | template: compileTemplate(`
33 |
34 | `),
35 | })
36 | ```
37 |
38 | 这样,子组件中的 `` 就会最终应用 `.some-class` 的样式规则:
39 |
40 | ```css
41 | .some-class {
42 | /* ... */
43 | }
44 | ```
45 |
46 | 即使启用了 [样式隔离](style_isolation.md) ,外部样式类依然可用。
47 |
--------------------------------------------------------------------------------
/glass-easel/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glass-easel",
3 | "description": "The core module of the glass-easel project",
4 | "version": "0.17.2",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/wechat-miniprogram/glass-easel.git"
8 | },
9 | "keywords": ["glass-easel"],
10 | "author": "wechat-miniprogram",
11 | "license": "MIT",
12 | "bugs": {
13 | "url": "https://github.com/wechat-miniprogram/glass-easel/issues"
14 | },
15 | "homepage": "https://github.com/wechat-miniprogram/glass-easel",
16 | "main": "dist/glass_easel.all.js",
17 | "module": "dist/glass_easel.all.es.js",
18 | "types": "dist/glass_easel.d.ts",
19 | "scripts": {
20 | "doc": "typedoc src/index.ts --excludePrivate --excludeProtected --excludeInternal",
21 | "build": "rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
22 | "dev": "GLASS_EASEL_ARGS=--dev npm run build",
23 | "lint": "eslint --ext .js,.ts src",
24 | "test": "jest -c jest.config.js",
25 | "coverage": "jest -c jest.config.js --collect-coverage"
26 | },
27 | "devDependencies": {
28 | "glass-easel-template-compiler": "workspace:*"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "glass-easel-template-compiler"
3 | version = "0.17.2"
4 | authors = ["LastLeaf "]
5 | description = "The template compiler of the glass-easel project."
6 | license = "MIT"
7 | documentation = "https://github.com/wechat-miniprogram/glass-easel"
8 | repository = "https://github.com/wechat-miniprogram/glass-easel"
9 | homepage = "https://github.com/wechat-miniprogram/glass-easel"
10 | edition = "2018"
11 |
12 | [lib]
13 | crate-type = ["cdylib", "rlib", "staticlib"]
14 |
15 | [[bin]]
16 | name = "glass-easel-template-compiler"
17 | path = "src/main.rs"
18 |
19 | [features]
20 | default = ["js_bindings", "c_bindings"]
21 | js_bindings = []
22 | c_bindings = ["cbindgen"]
23 |
24 | [dependencies]
25 | cssparser = "0.34"
26 | entities = "1"
27 | lazy_static = "1"
28 | regex = "^1.10.4"
29 | serde = "1"
30 | serde_json = "1"
31 | serde-wasm-bindgen = "0.6"
32 | wasm-bindgen = "0.2.79"
33 | js-sys = "0.3"
34 | log = "0.4"
35 | console_log = "0.2"
36 | env_logger = "0.9"
37 | clap = "2"
38 | cbindgen = { version = "0.21", optional = true }
39 | compact_str = "0.7"
40 | sourcemap = "7.0.1"
41 |
42 | [package.metadata.wasm-pack.profile.release]
43 | wasm-opt = false
44 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glass-easel-template-compiler",
3 | "description": "The template compiler of the glass-easel project.",
4 | "version": "0.17.2",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/wechat-miniprogram/glass-easel.git"
8 | },
9 | "keywords": ["glass-easel"],
10 | "author": "wechat-miniprogram",
11 | "license": "MIT",
12 | "bugs": {
13 | "url": "https://github.com/wechat-miniprogram/glass-easel/issues"
14 | },
15 | "homepage": "https://github.com/wechat-miniprogram/glass-easel",
16 | "files": [
17 | "pkg/glass_easel_template_compiler*",
18 | "/main.js",
19 | "/module.js",
20 | "/README.md"
21 | ],
22 | "main": "main.js",
23 | "module": "module.js",
24 | "browser": "pkg/glass_easel_template_compiler.js",
25 | "types": "pkg/glass_easel_template_compiler.d.ts",
26 | "scripts": {
27 | "build": "wasm-pack build --target bundler && mv pkg/package.json pkg/package.json.orig && rollup -o pkg/glass_easel_template_compiler_bg.cjs.js -f cjs pkg/glass_easel_template_compiler_bg.js"
28 | },
29 | "sideEffects": [
30 | "pkg/glass_easel_template_compiler.js",
31 | "pkg/snippets/*"
32 | ]
33 | }
34 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-webpack-plugin/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glass-easel-miniprogram-webpack-plugin",
3 | "description": "The webpack plugin of the glass-easel project for MiniProgram file structure",
4 | "version": "0.17.2",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/wechat-miniprogram/glass-easel.git"
8 | },
9 | "keywords": [
10 | "glass-easel"
11 | ],
12 | "author": "wechat-miniprogram",
13 | "license": "MIT",
14 | "bugs": {
15 | "url": "https://github.com/wechat-miniprogram/glass-easel/issues"
16 | },
17 | "homepage": "https://github.com/wechat-miniprogram/glass-easel",
18 | "main": "index.js",
19 | "scripts": {
20 | "build": "tsc -p .",
21 | "lint": "eslint src/**/*.ts"
22 | },
23 | "peerDependencies": {
24 | "glass-easel": "workspace:*",
25 | "glass-easel-miniprogram-adapter": "workspace:*",
26 | "webpack": "^5.101.3"
27 | },
28 | "dependencies": {
29 | "chalk": "4",
30 | "chokidar": "^4.0.3",
31 | "glass-easel-stylesheet-compiler": "workspace:*",
32 | "glass-easel-template-compiler": "workspace:*",
33 | "source-map": "^0.7.4",
34 | "webpack-sources": "^3.2.1",
35 | "webpack-virtual-modules": "^0.5.0"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-typescript/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glass-easel-miniprogram-typescript",
3 | "description": "A checker for the glass-easel project in MiniProgram file structure",
4 | "version": "0.17.2",
5 | "engines": {
6 | "node": ">=20.0.0"
7 | },
8 | "bin": {
9 | "miniprogram-typescript-check": "dist/cli.js"
10 | },
11 | "repository": {
12 | "type": "git",
13 | "url": "https://github.com/wechat-miniprogram/glass-easel.git"
14 | },
15 | "keywords": [
16 | "glass-easel"
17 | ],
18 | "author": "wechat-miniprogram",
19 | "license": "MIT",
20 | "bugs": {
21 | "url": "https://github.com/wechat-miniprogram/glass-easel/issues"
22 | },
23 | "homepage": "https://github.com/wechat-miniprogram/glass-easel",
24 | "main": "dist/index.js",
25 | "types": "src/index.ts",
26 | "scripts": {
27 | "build": "tsc -p .",
28 | "dev": "tsc -w -p .",
29 | "lint": "eslint src/**/*.ts",
30 | "start": "node dist/cli.js",
31 | "build-and-start": "npm run build && node dist/cli.js"
32 | },
33 | "peerDependencies": {
34 | "typescript": "^5.2.2"
35 | },
36 | "dependencies": {
37 | "chalk": "4",
38 | "chokidar": "^4.0.3",
39 | "glass-easel-template-compiler": "workspace:*"
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/src/path.rs:
--------------------------------------------------------------------------------
1 | pub(crate) fn normalize(path: &str) -> String {
2 | let mut slices = vec![];
3 | for slice in path.split('/') {
4 | match slice {
5 | "." => {}
6 | ".." => {
7 | slices.pop();
8 | }
9 | _ => {
10 | slices.push(slice);
11 | }
12 | }
13 | }
14 | slices.join("/")
15 | }
16 |
17 | pub(crate) fn resolve(base: &str, rel: &str) -> String {
18 | let mut slices = vec![];
19 | let main = if rel.starts_with('/') {
20 | &rel[1..]
21 | } else {
22 | for slice in base.split('/') {
23 | match slice {
24 | "." => {}
25 | ".." => {
26 | slices.pop();
27 | }
28 | _ => {
29 | slices.push(slice);
30 | }
31 | }
32 | }
33 | rel
34 | };
35 | slices.pop();
36 | for slice in main.split('/') {
37 | match slice {
38 | "." => {}
39 | ".." => {
40 | slices.pop();
41 | }
42 | _ => {
43 | slices.push(slice);
44 | }
45 | }
46 | }
47 | slices.join("/")
48 | }
49 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/README.md:
--------------------------------------------------------------------------------
1 | # glass-easel-miniprogram-adapter
2 |
3 | An MiniProgram interface adapter for glass-easel, which exports a mini-program-like API, such as `Component` and `Behavior` .
4 |
5 | Refer to the [glass-easel](https://github.com/wechat-miniprogram/glass-easel) project for further details.
6 |
7 | ## Build
8 |
9 | `nodejs` toolchain should be globally installed.
10 |
11 | Install dependencies:
12 |
13 | ```sh
14 | npm install
15 | ```
16 |
17 | Build:
18 |
19 | ```sh
20 | npm run build
21 | ```
22 |
23 | Build Doc:
24 |
25 | ```sh
26 | npm run doc
27 | ```
28 |
29 | Test:
30 |
31 | ```sh
32 | npm test
33 | ```
34 |
35 | Test coverage:
36 |
37 | ```sh
38 | npm run coverage
39 | ```
40 |
41 | ## Limitations
42 |
43 | Supported component API is a subset of mini-program API. See `docs` and mini-program docs for details.
44 |
45 | Style isolation support is also limited.
46 |
47 | * The `component: true` in JSON files should only be used in components, not pages (this influences the default style isolation settings).
48 | * The `styleIsolation` options should be specified in JSON files, not JS `Component` options.
49 | * Please work with [glass-easel-miniprogram-webpack-plugin](../glass-easel-miniprogram-webpack-plugin/) so that WXSS content can be correctly handled.
50 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "scripts": {
3 | "init-dev": "pnpm i && npm run build",
4 | "build": "pnpm -r run build"
5 | },
6 | "devDependencies": {
7 | "@rollup/plugin-node-resolve": "^15.2.1",
8 | "@rollup/plugin-replace": "^5.0.2",
9 | "@rollup/plugin-terser": "^0.4.3",
10 | "@rollup/plugin-typescript": "^11.1.3",
11 | "@tsd/typescript": "^5.2.2",
12 | "@types/jest": "^26.0.24",
13 | "@types/node": "^20.19.11",
14 | "@typescript-eslint/eslint-plugin": "^6.21.0",
15 | "@typescript-eslint/parser": "^6.21.0",
16 | "eslint": "^7.32.0",
17 | "eslint-config-airbnb-base": "^14.2.1",
18 | "eslint-config-prettier": "^8.6.0",
19 | "eslint-plugin-import": "^2.22.1",
20 | "eslint-plugin-prettier": "^4.2.1",
21 | "eslint-plugin-promise": "^4.2.1",
22 | "jest": "^29.6.4",
23 | "jest-environment-jsdom": "^29.6.4",
24 | "jest-runner-tsd": "^6.0.0",
25 | "prettier": "^2.8.3",
26 | "rollup": "^3.29.1",
27 | "rollup-plugin-dts": "^6.0.2",
28 | "ts-jest": "^29.1.1",
29 | "ts-loader": "^9.4.4",
30 | "tsd-lite": "^0.8.0",
31 | "tslib": "^2.6.2",
32 | "typedoc": "^0.25.13",
33 | "typescript": "~5.2.2",
34 | "webpack": "^5.101.3",
35 | "webpack-cli": "^6.0.1",
36 | "webpack-sources": "^3.2.3"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-typescript/README.md:
--------------------------------------------------------------------------------
1 | # glass-easel-miniprogram-typescript
2 |
3 | An extra TypeScript checker for mini-program code running in glass-easel.
4 |
5 | Refer to the [glass-easel](https://github.com/wechat-miniprogram/glass-easel) project for further details.
6 |
7 | ## Features
8 |
9 | If the component is written in TypeScript, this tool can do some type-checking on expressions inside WXML.
10 |
11 | Firstly, the component `.ts` must export the component as default. For example:
12 |
13 | ```ts
14 | export default Component({
15 | data: {
16 | hello: 'world',
17 | },
18 | })
19 | ```
20 |
21 | Then in `.wxml` :
22 |
23 | ```xml
24 | {{ hello }}
25 |
26 |
27 | {{ heloo }}
28 | ```
29 |
30 | This tool is based on TypeScript (a.k.a. `tsserver` ) for type-checking.
31 |
32 | Note that it only report errors in component WXML files. For type errors in TS files, you should run standard TypeScript commands.
33 |
34 | ## Usage
35 |
36 | After install this module, the `miniprogram-typescript-check` tool should be available.
37 |
38 | See all options: `npx miniprogram-typescript-check --help`
39 |
40 | In most cases, this command is suggested: `npx miniprogram-typescript-check -p [PATH_TO_SRC]`
41 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/styling/style_isolation.md:
--------------------------------------------------------------------------------
1 | # 样式隔离
2 |
3 | glass-easel 的默认设置中,组件之间的样式是共享的。如果想将一个样式表仅用于一个组件,可以考虑启用 style scope 支持。
4 |
5 | 在 DOM 环境下, style scope 要求样式表预处理时对样式表中的 class 都添加一个前缀,例如:
6 |
7 | ```css
8 | .my-prefix--header {
9 | font-size: 1.5em;
10 | }
11 | .my-prefix--footer {
12 | font-size: 0.9em;
13 | }
14 | ```
15 |
16 | 通过 `glass-easel-stylesheet-compiler` 工具可以为样式表添加固定的前缀,具体用法请参考它的相关文档。
17 |
18 | 添加前缀后,可以注册一个 style scope :
19 |
20 | ```js
21 | const myStyleScope = componentSpace.styleScopeManager.register('my-prefix')
22 | ```
23 |
24 | 组件定义时,可以指定它使用这个 style scope :
25 |
26 | ```js
27 | export const myComponent = componentSpace.defineComponent({
28 | options: {
29 | styleScope: myStyleScope,
30 | },
31 | template: compileTemplate(`
32 |
33 |
34 | `)
35 | })
36 | ```
37 |
38 | 如果指定了 style scope ,模板中的所有 class 指定的样式将只应用含有前缀的 class 样式规则。
39 |
40 | 若想要同时应用无前缀和含前缀两种 class 样式规则,可以改用 `extraStyleScope` 选项:
41 |
42 | ```js
43 | export const myComponent = componentSpace.defineComponent({
44 | options: {
45 | extraStyleScope: myStyleScope,
46 | },
47 | template: compileTemplate(`
48 |
49 |
50 | `)
51 | })
52 | ```
53 |
54 | [自定义后端](../advanced/custom_backend.md) 对样式隔离的支持方式略有不同,请参考它的相关文档。
55 |
--------------------------------------------------------------------------------
/glass-easel/tests/tmpl/dev_mode.test.ts:
--------------------------------------------------------------------------------
1 | import { tmpl, domBackend, composedBackend, shadowBackend } from '../base/env'
2 | import * as glassEasel from '../../src'
3 |
4 | const getTmplDevArgs = (n: glassEasel.Node) =>
5 | (n as glassEasel.Node & { _$wxTmplDevArgs: glassEasel.template.TmplDevArgs })._$wxTmplDevArgs
6 |
7 | const testCases = (testBackend: glassEasel.GeneralBackendContext) => {
8 | test('attribute list', () => {
9 | const def = glassEasel.registerElement({
10 | options: {
11 | dynamicSlots: true,
12 | },
13 | template: tmpl(`
14 |
15 |
16 |
17 | `),
18 | })
19 | const elem = glassEasel.Component.createWithContext('root', def.general(), testBackend)
20 | const div = elem.getShadowRoot()!.getElementById('a')!
21 | expect(getTmplDevArgs(div).A).toStrictEqual([':id', ':slot', ':class', ':style', 'hidden'])
22 | const slot = div.childNodes[0]!
23 | expect(getTmplDevArgs(slot).A).toStrictEqual([':name', 'v'])
24 | })
25 | }
26 |
27 | describe('event bindings (DOM backend)', () => testCases(domBackend))
28 | describe('event bindings (shadow backend)', () => testCases(shadowBackend))
29 | describe('event bindings (composed backend)', () => testCases(composedBackend))
30 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/tests/group.rs:
--------------------------------------------------------------------------------
1 | use glass_easel_template_compiler::*;
2 |
3 | #[test]
4 | fn basic_include() {
5 | const SRC_A: &str = r#" "#;
6 | const SRC_B: &str = r#" "#;
7 | let mut group = TmplGroup::new();
8 | group.add_tmpl("a", SRC_A);
9 | group.add_tmpl("b", SRC_B);
10 | assert_eq!(
11 | group.direct_dependencies("a").unwrap().collect::>(),
12 | Vec::::new()
13 | );
14 | assert_eq!(
15 | group.direct_dependencies("b").unwrap().collect::>(),
16 | vec!["a".to_string()]
17 | );
18 | }
19 |
20 | #[test]
21 | fn basic_import() {
22 | const SRC_A: &str =
23 | r#" "#;
24 | const SRC_B: &str = r#" "#;
25 | let mut group = TmplGroup::new();
26 | group.add_tmpl("a", SRC_A);
27 | group.add_tmpl("b", SRC_B);
28 | assert_eq!(
29 | group.direct_dependencies("a").unwrap().collect::>(),
30 | Vec::::new()
31 | );
32 | assert_eq!(
33 | group.direct_dependencies("b").unwrap().collect::>(),
34 | vec!["a".to_string()]
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "glass-easel-miniprogram-adapter",
3 | "description": "The MiniProgram interface adapter of the glass-easel project",
4 | "version": "0.17.2",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/wechat-miniprogram/glass-easel.git"
8 | },
9 | "keywords": ["glass-easel"],
10 | "author": "wechat-miniprogram",
11 | "license": "MIT",
12 | "bugs": {
13 | "url": "https://github.com/wechat-miniprogram/glass-easel/issues"
14 | },
15 | "homepage": "https://github.com/wechat-miniprogram/glass-easel",
16 | "main": "dist/glass_easel_miniprogram_adapter.js",
17 | "module": "dist/glass_easel_miniprogram_adapter.es.js",
18 | "types": "dist/glass_easel_miniprogram_adapter.d.js",
19 | "scripts": {
20 | "doc": "typedoc src/index.ts --excludePrivate --excludeProtected --excludeInternal",
21 | "build": "rollup --config rollup.config.ts --configPlugin @rollup/plugin-typescript",
22 | "dev": "GLASS_EASEL_ARGS=--dev npm run build",
23 | "lint": "eslint --ext .ts src",
24 | "test": "jest -c jest.config.js",
25 | "coverage": "jest -c jest.config.js --collect-coverage"
26 | },
27 | "peerDependencies": {
28 | "glass-easel": "workspace:*"
29 | },
30 | "devDependencies": {
31 | "glass-easel": "workspace:*",
32 | "glass-easel-template-compiler": "workspace:*"
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/glass-easel-stylesheet-compiler/README.md:
--------------------------------------------------------------------------------
1 | # glass-easel-stylesheet-compiler
2 |
3 | The stylesheet compiler for the glass-easel project.
4 |
5 | This tool can help:
6 |
7 | * convert `rpx` to `vw` ;
8 | * work with style isolation options through class-prefixes;
9 | * minify the output CSS.
10 |
11 | Refer to the [glass-easel](https://github.com/wechat-miniprogram/glass-easel) project for further details.
12 |
13 | ## Build
14 |
15 | `rust` toolchain and `wasm-pack` should be globally installed.
16 |
17 | Build WebAssembly binary:
18 |
19 | ```sh
20 | wasm-pack build glass-easel-stylesheet-compiler --target nodejs --out-dir pkg-nodejs
21 | ```
22 |
23 | Build binary:
24 |
25 | ```sh
26 | cargo build --release
27 | ```
28 |
29 | ## JavaScript Interface
30 |
31 | This tool can be used in webpack, i.e. [glass-easel-miniprogram-webpack-plugin](../glass-easel-miniprogram-webpack-plugin/) .
32 |
33 | However, if you want to call it directly, see the example below.
34 |
35 | ```js
36 | const { StyleSheetTransformer } = require('glass-easel-stylesheet-compiler')
37 |
38 | // convert a CSS file
39 | const rpxRatio = 750
40 | const sst = new StyleSheetTransformer(PATH, CONTENT, CLASS_PREFIX, rpxRatio)
41 |
42 | // get the CSS output
43 | const ss = sst.getContent()
44 |
45 | // get the source map if needed
46 | sst.toSourceMap()
47 |
48 | // free it if the source map is not required
49 | sst.free()
50 | ```
51 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/interaction/behavior.md:
--------------------------------------------------------------------------------
1 | # 普通 Behaviors
2 |
3 | behaviors 是一种简单的组件间代码共享机制。通过 behaviors ,可以将几个组件间共享的部分逻辑抽离出来。
4 |
5 | 对于 Definition API ,可以用 behaviors 定义一部分属性、方法等,然后在组件中可以引用 behaviors ,例如:
6 |
7 | ```js
8 | export const sharedBehavior = componentSpace.defineComponent({
9 | // 属性列表会被合并到引用它的组件中
10 | properties: {
11 | a: Number,
12 | },
13 | })
14 |
15 | export const myComponent = componentSpace.defineComponent({
16 | // 引入 behavior
17 | behaviors: [sharedBehavior],
18 | properties: {
19 | b: String,
20 | },
21 | // 属性会与 behavior 中的属性列表合并,因而模板中可以引用模板中的字段
22 | template: compileTemplate(`
23 | {{a}}
24 | {{b}}
25 | `),
26 | })
27 | ```
28 |
29 | 注意,有些字段不能写在 behaviors 中(会被忽略)。
30 |
31 | * `template` 模板因为无法很好合并,所以不能写在 behaviors 中。
32 | * `using` `generics` `placeholder` 由于涉及了模板中节点信息,也不能写在 behaviors 中。
33 | * `options` 是针对组件的配置,不能写在 behaviors 中。
34 |
35 | 对于 Chaining API , behaviors 相当于链式调用中的一部分。 Chaining API 的 behaviors 也有上述字段限制。
36 |
37 | 上面的例子可以用 Chaining API 表达为:
38 |
39 | ```js
40 | export const sharedBehavior = componentSpace.define()
41 | .property('a', Number)
42 | .registerBehavior()
43 |
44 | export const myComponent = componentSpace.define()
45 | .behavior(sharedBehavior)
46 | .property('b', String)
47 | .template(compileTemplate(`
48 | {{a}}
49 | {{b}}
50 | `))
51 | .registerComponent()
52 | ```
53 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/src/template_engine.ts:
--------------------------------------------------------------------------------
1 | import type {
2 | GeneralBehavior,
3 | GeneralComponent,
4 | NormalizedComponentOptions,
5 | ShadowRoot,
6 | templateEngine,
7 | } from 'glass-easel'
8 |
9 | export class EmptyTemplateEngine implements templateEngine.Template {
10 | static create(behavior: GeneralBehavior, componentOptions: NormalizedComponentOptions) {
11 | return new EmptyTemplateEngine(componentOptions.externalComponent)
12 | }
13 |
14 | // eslint-disable-next-line no-useless-constructor
15 | constructor(private externalComponent: boolean) {
16 | //
17 | }
18 |
19 | createInstance(
20 | comp: GeneralComponent,
21 | createShadowRoot: (component: GeneralComponent) => ShadowRoot,
22 | ): templateEngine.TemplateInstance {
23 | const instance: templateEngine.TemplateInstance = {
24 | shadowRoot: this.externalComponent
25 | ? {
26 | root: comp.getBackendElement()!,
27 | slot: comp.getBackendElement()!,
28 | getIdMap: () => ({}),
29 | handleEvent: () => {
30 | // empty
31 | },
32 | setListener: () => {
33 | // empty
34 | },
35 | }
36 | : createShadowRoot(comp),
37 | initValues: (_data) => {
38 | // empty
39 | },
40 | updateValues: (_data, _changes) => {
41 | // empty
42 | },
43 | }
44 |
45 | return instance
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/styling/virtual_host.md:
--------------------------------------------------------------------------------
1 | # 虚拟组件节点
2 |
3 | 默认情况下,代表组件的节点本身会和普通节点一样接收 class 和 style ,例如:
4 |
5 | ```js
6 | export const myComponent = componentSpace.defineComponent({
7 | using: {
8 | child: childComponent,
9 | },
10 | template: compileTemplate(`
11 |
12 |
13 | `)
14 | })
15 | ```
16 |
17 | 在 DOM 节点树上, `` 节点本身也会生成一个对应的 DOM 节点,使其能直接使用 class 和 style ;但这也使得一些情况下布局难以控制,例如在使用 flex 布局时, `` 节点本身会成为一个 flex 内容项。
18 |
19 | 通过指定组件的 `virtualHost` 选项,可以改变这一行为,使其本身不生成一个对应的 DOM 节点,这样:
20 |
21 | * flex 布局时,它本身不作为 flex 内容项参与布局;
22 | * 节点本身的 class 和 style 失效。
23 |
24 | 虽然节点本身的 class 和 style 会失效,但可以将 class 定义为一个 [外部样式类](external_class.md) 、可以将 style 定义为一个属性,这样就可以用上它们,例如:
25 |
26 | ```js
27 | export const childComponent = componentSpace.defineComponent({
28 | options: {
29 | virtualHost: true,
30 | },
31 | // 将 class 作为一个外部样式类
32 | externalClasses: ['class'],
33 | // 将 style 作为一个属性
34 | properties: {
35 | style: String,
36 | },
37 | // 将 class 和 style 应用到组件内部的节点上
38 | template: compileTemplate(`
39 |
40 | `),
41 | })
42 |
43 | export const myComponent = componentSpace.defineComponent({
44 | using: {
45 | child: childComponent,
46 | },
47 | // 同样可以使用 class 和 style
48 | template: compileTemplate(`
49 |
50 | `)
51 | })
52 | ```
53 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/src/entities.rs:
--------------------------------------------------------------------------------
1 | //! Helpers for parsing HTML entities
2 |
3 | use entities::ENTITIES;
4 | use std::{borrow::Cow, collections::HashMap};
5 |
6 | lazy_static! {
7 | static ref ENTITIES_MAPPING: HashMap<&'static str, &'static str> = make_mapping();
8 | }
9 |
10 | fn make_mapping() -> HashMap<&'static str, &'static str> {
11 | let mut mapping = HashMap::new();
12 | for entity in ENTITIES.iter() {
13 | mapping.insert(entity.entity, entity.characters);
14 | }
15 | mapping
16 | }
17 |
18 | pub(crate) fn decode(entity: &str) -> Option> {
19 | let len = entity.len();
20 | if &entity[(len - 1)..] != ";" {
21 | return None;
22 | }
23 | if len > 4 && &entity[1..=2] == "#x" {
24 | let hex_str = &entity[3..(len - 1)];
25 | if let Ok(hex) = u32::from_str_radix(hex_str, 16) {
26 | if let Some(c) = char::from_u32(hex) {
27 | return Some(Cow::Owned(String::from(c)));
28 | }
29 | }
30 | return None;
31 | }
32 | if len > 3 && &entity[1..=1] == "#" {
33 | let digit_str = &entity[2..(len - 1)];
34 | if let Ok(hex) = u32::from_str_radix(digit_str, 10) {
35 | if let Some(c) = char::from_u32(hex) {
36 | return Some(Cow::Owned(String::from(c)));
37 | }
38 | }
39 | return None;
40 | }
41 | ENTITIES_MAPPING.get(entity).map(|x| Cow::Borrowed(*x))
42 | }
43 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/tests/script.rs:
--------------------------------------------------------------------------------
1 | use glass_easel_template_compiler::*;
2 |
3 | #[test]
4 | fn external_script() {
5 | const SRC_A: &str = r#" {{ modA.a + modB.b }}"#;
6 | const SRC_SCRIPT: &str = r#"(function(){return 0})()"#;
7 | let mut group = TmplGroup::new();
8 | group.add_tmpl("tmpl/a", SRC_A);
9 | group.add_script("script/a", SRC_SCRIPT);
10 | group.add_script("script/b", SRC_SCRIPT);
11 | assert_eq!(
12 | group
13 | .script_dependencies("tmpl/a")
14 | .unwrap()
15 | .collect::>(),
16 | vec!["script/a".to_string(), "script/b".to_string()],
17 | );
18 | }
19 |
20 | #[test]
21 | fn inline_script() {
22 | const SRC_A: &str = r#"{{ modA.hi }}
exports.hi = 1 < 2 "#;
23 | let mut group = TmplGroup::new();
24 | group.add_tmpl("tmpl/a", SRC_A);
25 | assert_eq!(group.script_dependencies("tmpl/a").unwrap().count(), 0);
26 | assert_eq!(
27 | group
28 | .inline_script_module_names("tmpl/a")
29 | .unwrap()
30 | .collect::>(),
31 | vec!["modA".to_string(), "modB".to_string()],
32 | );
33 | assert_eq!(
34 | group.inline_script_content("tmpl/a", "modA").unwrap(),
35 | " exports.hi = 1 < 2 "
36 | );
37 | assert_eq!(group.inline_script_content("tmpl/a", "modB").unwrap(), "");
38 | }
39 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/basic/method.md:
--------------------------------------------------------------------------------
1 | # 方法
2 |
3 | ## 普通组件方法
4 |
5 | 可以通过 `methods` 来定义一些能在组件实例 `this` 对象上访问的函数:
6 |
7 | ```js
8 | // 使用 Definition API 添加方法
9 | export const myComponent = componentSpace.defineComponent({
10 | methods: {
11 | aPlusB(a, b) {
12 | return a + b
13 | },
14 | },
15 | lifetimes: {
16 | attached() {
17 | this.aPlusB(1, 2) === 3 // true
18 | },
19 | },
20 | })
21 | // 或使用 Chaining API 添加方法
22 | export const myComponent = componentSpace.define()
23 | .methods({
24 | aPlusB(a, b) {
25 | return a + b
26 | },
27 | })
28 | .lifetime('attached', function () {
29 | this.aPlusB(1, 2) === 3 // true
30 | })
31 | .registerComponent()
32 | // 或在 init 中添加方法
33 | export const myComponent = componentSpace.define()
34 | .init(function ({ lifetime, method }) {
35 | const aPlusB = method((a, b) => {
36 | return a + b
37 | })
38 | lifetime('attached', function () {
39 | this.aPlusB(1, 2) === 3 // true
40 | })
41 | // 注意 method 需要返回
42 | return { aPlusB }
43 | })
44 | .registerComponent()
45 | ```
46 |
47 | ## 使用 init 内函数代替
48 |
49 | 实际上,如果使用 init ,则通常不需要用普通组件方法,直接在 init 中定义函数即可。
50 |
51 | ```js
52 | export const myComponent = componentSpace.define()
53 | .init(function ({ lifetime }) {
54 | const aPlusB = (a, b) => a + b
55 | lifetime('attached', function () {
56 | aPlusB(1, 2) === 3 // true
57 | })
58 | })
59 | .registerComponent()
60 | ```
61 |
62 | 这种做法可以让一些私有函数不能通过组件实例 `this` 来访问。
63 |
--------------------------------------------------------------------------------
/glass-easel/src/type_symbol.ts:
--------------------------------------------------------------------------------
1 | import { type GeneralComponent } from './component'
2 | import { type Element } from './element'
3 | import { type NativeNode } from './native_node'
4 | import { type ShadowRoot } from './shadow_root'
5 | import { type TextNode } from './text_node'
6 | import { type VirtualNode } from './virtual_node'
7 |
8 | export const TEXT_NODE_SYMBOL = Symbol('TextNode')
9 |
10 | export const ELEMENT_SYMBOL = Symbol('Element')
11 |
12 | export const NATIVE_NODE_SYMBOL = Symbol('NativeNode')
13 |
14 | export const VIRTUAL_NODE_SYMBOL = Symbol('VirtualNode')
15 |
16 | export const SHADOW_ROOT_SYMBOL = Symbol('ShadowRootSymbol')
17 |
18 | export const COMPONENT_SYMBOL = Symbol('Component')
19 |
20 | export const isTextNode = (e: any): e is TextNode =>
21 | !!e && (e as { [TEXT_NODE_SYMBOL]: boolean })[TEXT_NODE_SYMBOL]
22 |
23 | export const isElement = (e: any): e is Element =>
24 | !!e && (e as { [ELEMENT_SYMBOL]: boolean })[ELEMENT_SYMBOL]
25 |
26 | export const isNativeNode = (e: any): e is NativeNode =>
27 | !!e && (e as { [NATIVE_NODE_SYMBOL]: boolean })[NATIVE_NODE_SYMBOL]
28 |
29 | export const isVirtualNode = (e: any): e is VirtualNode =>
30 | !!e && (e as { [VIRTUAL_NODE_SYMBOL]: boolean })[VIRTUAL_NODE_SYMBOL]
31 |
32 | export const isShadowRoot = (e: any): e is ShadowRoot =>
33 | !!e && (e as { [SHADOW_ROOT_SYMBOL]: boolean })[SHADOW_ROOT_SYMBOL]
34 |
35 | export const isComponent = (e: any): e is GeneralComponent =>
36 | !!e && (e as { [COMPONENT_SYMBOL]: boolean })[COMPONENT_SYMBOL]
37 |
--------------------------------------------------------------------------------
/glass-easel/tests/core/data_proxy.test.ts:
--------------------------------------------------------------------------------
1 | import { domBackend } from '../base/env'
2 | import * as glassEasel from '../../src'
3 |
4 | const componentSpace = new glassEasel.ComponentSpace()
5 | componentSpace.updateComponentOptions({
6 | writeFieldsToNode: true,
7 | writeIdToDOM: true,
8 | })
9 | componentSpace.defineComponent({
10 | is: '',
11 | })
12 |
13 | describe('dynamic add observer', () => {
14 | it('should trigger data observers', () => {
15 | let actionOrder: number[] = []
16 |
17 | const Comp = componentSpace.defineComponent({
18 | observers: {
19 | data1() {
20 | actionOrder.push(1)
21 | },
22 | },
23 | })
24 |
25 | const elem = glassEasel.Component.createWithContext('root', Comp, domBackend).general()
26 | const elem2 = glassEasel.Component.createWithContext('root', Comp, domBackend).general()
27 |
28 | elem.setData({
29 | data1: 1,
30 | data2: 2,
31 | })
32 | expect(actionOrder).toStrictEqual([1])
33 | actionOrder = []
34 |
35 | elem.dynamicAddObserver(() => {
36 | actionOrder.push(2)
37 | }, 'data2')
38 | elem.dynamicAddObserver(() => {
39 | actionOrder.push(3)
40 | }, '**')
41 | elem.setData({
42 | data1: 11,
43 | data2: 22,
44 | })
45 | expect(actionOrder).toStrictEqual([1, 2, 3])
46 | actionOrder = []
47 |
48 | elem2.setData({
49 | data1: 1,
50 | data2: 2,
51 | })
52 | expect(actionOrder).toStrictEqual([1])
53 | actionOrder = []
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/deprecate.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-console */
2 |
3 | const childProcess = require('child_process')
4 |
5 | // check arguments
6 | const version = process.argv[2]
7 | if (!version) {
8 | throw new Error('version not given in argv')
9 | }
10 | if (!/[0-9]+\.[0-9]+\.[0-9]+/.test(version)) {
11 | throw new Error('version illegal')
12 | }
13 | const message = process.argv[3]
14 | if (!message) {
15 | throw new Error('message not given in argv')
16 | }
17 |
18 | // npm deprecate
19 | ;[
20 | 'glass-easel-template-compiler',
21 | 'glass-easel-stylesheet-compiler',
22 | 'glass-easel',
23 | 'glass-easel-miniprogram-adapter',
24 | 'glass-easel-miniprogram-webpack-plugin',
25 | 'glass-easel-miniprogram-typescript',
26 | 'glass-easel-miniprogram-template',
27 | 'glass-easel-shadow-sync',
28 | ].forEach((p) => {
29 | console.info(`Deprecate ${p}@${version} on npmjs`)
30 | if (
31 | childProcess.spawnSync(
32 | 'npm',
33 | ['deprecate', `${p}@${version}`, message, '--registry', 'https://registry.npmjs.org'],
34 | { stdio: 'inherit' },
35 | ).status !== 0
36 | ) {
37 | throw new Error('failed to deprecate')
38 | }
39 | })
40 |
41 | // cargo yank
42 | ;['glass-easel-template-compiler', 'glass-easel-stylesheet-compiler'].forEach((p) => {
43 | console.info(`Deprecate ${p}@${version} on crates.io`)
44 | if (
45 | childProcess.spawnSync('cargo', ['yank', '--version', `${version}`, `${p}`], {
46 | stdio: 'inherit',
47 | }).status !== 0
48 | ) {
49 | throw new Error('failed to deprecate')
50 | }
51 | })
52 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/src/stringify/mod.rs:
--------------------------------------------------------------------------------
1 | pub use sourcemap::SourceMap;
2 |
3 | pub use options::StringifyOptions;
4 | pub use stringifier::*;
5 |
6 | pub(crate) mod expr;
7 | pub mod options;
8 | mod stringifier;
9 | mod tag;
10 | pub(crate) mod typescript;
11 |
12 | fn is_typescript_keyword(s: &str) -> bool {
13 | const TS_KEYWORDS: [&'static str; 53] = [
14 | "break",
15 | "case",
16 | "catch",
17 | "class",
18 | "const",
19 | "continue",
20 | "debugger",
21 | "default",
22 | "delete",
23 | "do",
24 | "else",
25 | "export",
26 | "extends",
27 | "finally",
28 | "for",
29 | "function",
30 | "if",
31 | "import",
32 | "in",
33 | "instanceof",
34 | "new",
35 | "return",
36 | "super",
37 | "switch",
38 | "this",
39 | "throw",
40 | "try",
41 | "typeof",
42 | "var",
43 | "void",
44 | "while",
45 | "with",
46 | "yield",
47 | "enum",
48 | "await",
49 | "implements",
50 | "interface",
51 | "let",
52 | "package",
53 | "private",
54 | "protected",
55 | "public",
56 | "static",
57 | "arguments",
58 | "eval",
59 | "type",
60 | "namespace",
61 | "module",
62 | "declare",
63 | "as",
64 | "is",
65 | "keyof",
66 | "readonly",
67 | ];
68 | TS_KEYWORDS.contains(&s)
69 | }
70 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/rollup.config.mjs:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import nodeResolve from '@rollup/plugin-node-resolve'
3 | import typescript from '@rollup/plugin-typescript'
4 | import dts from 'rollup-plugin-dts'
5 | import terser from '@rollup/plugin-terser'
6 |
7 | const config = [
8 | {
9 | input: './src/view_controller',
10 | output: [{ file: 'dist/view_controller.js', sourcemap: true, format: 'es' }],
11 | plugins: [
12 | nodeResolve({
13 | extensions: ['.ts', 'js'],
14 | }),
15 | typescript({
16 | sourceMap: true,
17 | }),
18 | terser({
19 | sourceMap: true,
20 | }),
21 | ],
22 | },
23 | {
24 | input: './src/view_controller',
25 | output: [{ file: 'dist/view_controller.d.ts', format: 'es' }],
26 | plugins: [
27 | nodeResolve({
28 | extensions: ['.ts', 'js'],
29 | }),
30 | dts(),
31 | ],
32 | },
33 | {
34 | input: './src/backend',
35 | output: [{ file: 'dist/backend.js', sourcemap: true, format: 'es' }],
36 | plugins: [
37 | nodeResolve({
38 | extensions: ['.ts', 'js'],
39 | }),
40 | typescript({
41 | sourceMap: true,
42 | }),
43 | terser({
44 | sourceMap: true,
45 | }),
46 | ],
47 | },
48 | {
49 | input: './src/backend',
50 | output: [{ file: 'dist/backend.d.ts', format: 'es' }],
51 | plugins: [
52 | nodeResolve({
53 | extensions: ['.ts', 'js'],
54 | }),
55 | dts(),
56 | ],
57 | },
58 | ]
59 |
60 | export default config
61 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/tree/element_iterator.md:
--------------------------------------------------------------------------------
1 | # 节点树遍历
2 |
3 | ## 使用节点树遍历器
4 |
5 | 通常,节点树遍历可以直接借助 `ElementIterator` 来实现。
6 |
7 | 例如,依次获得某个节点在 Shadow Tree 上的所有节点:
8 |
9 | ```js
10 | export const myComponent = componentSpace.defineComponent({
11 | lifetimes: {
12 | attached() {
13 | // 遍历模式: Shadow Tree 上所有子孙节点
14 | const mode = 'shadow-descendants-root-first'
15 | // 遍历
16 | glassEasel.ElementIterator.create(this.shadowRoot, mode)
17 | .forEach((node) => {
18 | // 依次回调所有子孙节点
19 | // 返回 false 会中断遍历
20 | })
21 | },
22 | },
23 | })
24 | ```
25 |
26 | 节点树遍历模式有以下几种。
27 |
28 | | 模式 | 遍历范围 | 遍历顺序 |
29 | | ---- | -------- | -------- |
30 | | shadow-ancestors | Shadow Tree | 当前节点的祖先节点,由近及远 |
31 | | shadow-descendants-root-first | Shadow Tree | 当前节点的子孙节点,先父节点再子节点(先根遍历) |
32 | | shadow-descendants-root-last | Shadow Tree | 当前节点的子孙节点,先子节点再父节点(后根遍历) |
33 | | composed-ancestors | Composed Tree | 当前节点的祖先节点,由近及远 |
34 | | composed-descendants-root-first | Composed Tree | 当前节点的子孙节点,先父节点再子节点(先根遍历) |
35 | | composed-descendants-root-last | Composed Tree | 当前节点的子孙节点,先子节点再父节点(后根遍历) |
36 |
37 | ## 限制节点遍历器返回的节点类型
38 |
39 | 常常,遍历时只需要关心某一类节点。例如,遍历过程中只需要处理 `glassEasel.Element` ,不需要 `glassEasel.TextNode` ,此时可以限制 `ElementIterator` 的返回类型:
40 |
41 | ```js
42 | export const myComponent = componentSpace.defineComponent({
43 | lifetimes: {
44 | attached() {
45 | const mode = 'shadow-descendants-root-first'
46 | // 仅返回 glassEasel.Element
47 | glassEasel.ElementIterator.create(this.shadowRoot, mode, glassEasel.Element)
48 | .forEach((node) => {
49 | // 依次回调所有 glassEasel.Element 节点
50 | })
51 | },
52 | },
53 | })
54 | ```
55 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/interaction/generic.md:
--------------------------------------------------------------------------------
1 | # 抽象节点
2 |
3 | 有时,由于代码解耦等需要,一些节点对应的组件是不确定的。此时可以将它设置为一个 **抽象节点** ,它对应的组件可以由父组件来指定。例如:
4 |
5 | ```js
6 | // 使用 Definition API 设置抽象节点
7 | export const childComponent = componentSpace.defineComponent({
8 | generics: {
9 | // 设置一个抽象节点
10 | item: true,
11 | },
12 | // 在模板中可以使用抽象节点
13 | template: compileTemplate(`
14 |
15 | `),
16 | })
17 | // 或使用 Chaining API 设置抽象节点
18 | export const childComponent = componentSpace.define()
19 | .generics({
20 | item: true,
21 | })
22 | .template(compileTemplate(`
23 |
24 | `))
25 | .registerComponent()
26 | ```
27 |
28 | 在使用含有抽象节点的组件时,需要指定抽象节点对应的实际组件,例如:
29 |
30 | ```js
31 | // 先定义一个抽象节点的实现组件
32 | export const implItemComponent = componentSpace.defineComponent({})
33 |
34 | // 使用组件时,传入 itemComponent
35 | export const myComponent = componentSpace.defineComponent({
36 | using: {
37 | child: childComponent,
38 | impl: implItemComponent,
39 | },
40 | template: compileTemplate(`
41 |
42 | `),
43 | })
44 | ```
45 |
46 | 注意:使用 `generic:` 传入实现组件时,不能使用数据绑定。
47 |
48 | 定义抽象节点时,可以提供一个默认组件,如果父组件没有传入实现组件,则会转而使用这个默认组件,例如:
49 |
50 | ```js
51 | const defaultComponent = componentSpace.defineComponent({})
52 |
53 | export const childComponent = componentSpace.defineComponent({
54 | generics: {
55 | item: {
56 | default: defaultComponent,
57 | },
58 | },
59 | template: compileTemplate(`
60 |
61 | `),
62 | })
63 |
64 | export const myComponent = componentSpace.defineComponent({
65 | using: {
66 | child: childComponent,
67 | },
68 | // 未指定 generic:item 时,默认组件将被使用
69 | template: compileTemplate(`
70 |
71 | `),
72 | })
73 | ```
74 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/advanced/template_engine.md:
--------------------------------------------------------------------------------
1 | # 自定义模板引擎
2 |
3 | ## 模板引擎简介
4 |
5 | 模板引擎是处理模板、在模板上应用数据绑定更新的模块。
6 |
7 | glass-easel 的默认模板引擎是 `glassEasel.glassEaselTemplate` ,但也可以通过更改组件 options 来指定另一个自定义的模块作为模板引擎。
8 |
9 | 自定义模板引擎必须实现 TypeScript 接口 `glassEasel.templateEngine.TemplateEngine` 。
10 |
11 | ## 模板引擎接口
12 |
13 | `glassEasel.templateEngine.TemplateEngine` 接口需要实现一个 `create` 方法。这个方法对每个组件定义执行一次。它接受传入的组件定义,传出一个 `glassEasel.templateEngine.Template` 。
14 |
15 | `glassEasel.templateEngine.Template` 接口需要实现一个 `createInstance` 方法。这个方法在每个组件实例创建时执行一次。它需要生成一个 `glassEasel.templateEngine.TemplateInstance` 。
16 |
17 | `glassEasel.templateEngine.TemplateInstance` 则需要提供一个 `shadowRoot` 节点(调用 `glassEasel.ShadowRoot.createShadowRoot` 来获得)作为组件实例的 `this.shadowRoot` 。还需要实现 `initValues` 和 `updateValues` ,分别用于创建初始节点树、更新数据绑定。
18 |
19 | 实现模板引擎时,常常需要用到 [节点树变更](../tree/node_tree_modification.md) 接口。
20 |
21 | ## 自定义模板引擎示例
22 |
23 | 自定义模板引擎的实现可以大体上以下例表示:
24 |
25 | ```js
26 | class MyTemplateEngine {
27 | create(rootBehavior, options) {
28 | behavior.getTemplate() // 组件的 template 字段内容
29 | return new MyTemplate()
30 | }
31 | }
32 |
33 | class MyTemplate {
34 | createInstance(component) {
35 | return new MyTemplateInstance(component)
36 | }
37 | }
38 |
39 | class MyTemplateInstance {
40 | constructor(component) {
41 | this.shadowRoot = ShadowRoot.createShadowRoot(component)
42 | }
43 |
44 | initValues(data) {
45 | // 组件被创建,初始数据是 data
46 | }
47 |
48 | updateValues(data, changes) {
49 | // 组件更新,新的数据是 data ,变更内容描述在 changes 中
50 | }
51 | }
52 |
53 | export const myComponent = componentSpace.defineComponent({
54 | options: {
55 | templateEngine: new MyTemplateEngine(),
56 | },
57 | template: {
58 | // 这部分内容由 MyTemplateEngine 来处理
59 | },
60 | })
61 | ```
62 |
--------------------------------------------------------------------------------
/glass-easel/src/trait_behaviors.ts:
--------------------------------------------------------------------------------
1 | import { type ComponentSpace } from './component_space'
2 |
3 | /**
4 | * Interface that can be implement dynamically
5 | *
6 | * A `TraitBehavior` is like a TypeScript interface, but can be implemented dynamically.
7 | * It requires the implementors to implement `TIn` .
8 | * Also, it can provide do a transform from `TIn` to `TOut` as common logic of the trait.
9 | */
10 | export class TraitBehavior<
11 | TIn extends { [key: string]: unknown },
12 | TOut extends { [key: string]: unknown } = TIn,
13 | > {
14 | /** @internal */
15 | private _$trans?: (impl: TIn) => TOut
16 | ownerSpace: ComponentSpace
17 |
18 | /** @internal */
19 | // eslint-disable-next-line no-useless-constructor
20 | constructor(ownerSpace: ComponentSpace, trans?: (impl: TIn) => TOut) {
21 | this.ownerSpace = ownerSpace
22 | this._$trans = trans
23 | }
24 |
25 | /** @internal */
26 | _$implement(impl: TIn): TOut {
27 | return this._$trans?.(impl) || (impl as unknown as TOut)
28 | }
29 | }
30 |
31 | /**
32 | * A manager that can implement multiple different trait behaviors
33 | */
34 | export class TraitGroup {
35 | private _$traits: WeakMap, unknown> = new WeakMap()
36 |
37 | implement(
38 | traitBehavior: TraitBehavior,
39 | impl: TIn,
40 | ) {
41 | const traitImpl = traitBehavior._$implement(impl)
42 | this._$traits.set(traitBehavior, traitImpl)
43 | }
44 |
45 | get(
46 | traitBehavior: TraitBehavior,
47 | ): TOut | undefined {
48 | return this._$traits.get(traitBehavior) as TOut | undefined
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/advanced/component_filter.md:
--------------------------------------------------------------------------------
1 | # 组件构造器中间件
2 |
3 | ## Definition Filter
4 |
5 | 在定义组件时, glass-easel 允许一些简易的中间件来调整组件定义。
6 |
7 | 对于 definition API ,可以使用 `definitionFilter` 。例如,可以定义一个中间件,用来检查是否有属性没有定义初始值:
8 |
9 | ```js
10 | export const propCheck = componentSpace.defineBehavior({
11 | definitionFilter(def) {
12 | // def 是定义组件时传入的组件定义对象
13 | if (def.properties) {
14 | Object.keys(def.properties).forEach((propName) => {
15 | const prop = def.properties[propName]
16 | if (prop.value === undefined) {
17 | console.warn(`Forget property initial value of "${propName}"`)
18 | }
19 | })
20 | }
21 | }
22 | })
23 | ```
24 |
25 | 中间件表现为一个 behavior 的形式。在组件中引用这个 behavior 时,中间件会被自动触发,例如:
26 |
27 | ```js
28 | export const myComponent = componentSpace.defineComponent({
29 | // 引入含有 definitionFilter 的 behavior
30 | behaviors: [propCheck],
31 | properties: {
32 | a: {
33 | type: String,
34 | },
35 | },
36 | })
37 | ```
38 |
39 | ## Chaining Filter
40 |
41 | 对于 Chaining API ,可以使用 `chainingFilter` 。它允许修改链式调用中的调用链函数。例如:
42 |
43 | ```js
44 | export const propCheck = componentSpace.define()
45 | .chainingFilter((chain) => {
46 | return Object.create(chain, {
47 | property: {
48 | value(propName, prop) {
49 | if (prop.value === undefined) {
50 | console.warn(`Forget property initial value of "${propName}"`)
51 | }
52 | chain.property(propName, prop)
53 | }
54 | },
55 | })
56 | })
57 | .registerBehavior()
58 | ```
59 |
60 | 使用这个 behavior 时,要在链式调用靠前的地方引用:
61 |
62 | ```js
63 | export const myComponent = componentSpace.define()
64 | // 引入含有 chainingFilter 的 behavior
65 | .behavior(propCheck)
66 | .property('a', {
67 | type: String,
68 | })
69 | .registerComponent()
70 | ```
71 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/src/resize.ts:
--------------------------------------------------------------------------------
1 | import * as glassEasel from 'glass-easel'
2 | import { type GeneralComponent } from './component'
3 |
4 | export class ResizeObserver {
5 | private _$comp: GeneralComponent
6 | private _$observeAll: boolean
7 | private _$mode = glassEasel.backend.ResizeObserverMode.ContentBox
8 | private _$observers: glassEasel.backend.Observer[] = []
9 |
10 | /** @internal */
11 | constructor(comp: GeneralComponent, observeAll: boolean) {
12 | this._$comp = comp
13 | this._$observeAll = observeAll
14 | }
15 |
16 | contentBox(): this {
17 | this._$mode = glassEasel.backend.ResizeObserverMode.ContentBox
18 | return this
19 | }
20 |
21 | borderBox(): this {
22 | this._$mode = glassEasel.backend.ResizeObserverMode.BorderBox
23 | return this
24 | }
25 |
26 | observe(targetSelector: string, listener: (status: glassEasel.backend.ResizeStatus) => void) {
27 | const shadowRoot = this._$comp._$.getShadowRoot()
28 | let targets: glassEasel.Element[]
29 | if (!shadowRoot) {
30 | targets = []
31 | } else if (this._$observeAll) {
32 | targets = shadowRoot.querySelectorAll(targetSelector)
33 | } else {
34 | const elem = shadowRoot.querySelector(targetSelector)
35 | if (elem === null) {
36 | targets = []
37 | } else {
38 | targets = [elem]
39 | }
40 | }
41 | targets.forEach((target) => {
42 | const observer = target.createResizeObserver(this._$mode, listener)
43 | if (observer === null) {
44 | // TODO warn no observer attached
45 | } else {
46 | this._$observers.push(observer)
47 | }
48 | })
49 | }
50 |
51 | disconnect() {
52 | const observers = this._$observers
53 | this._$observers = []
54 | observers.forEach((observer) => observer.disconnect())
55 | }
56 | }
57 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/advanced/custom_backend.md:
--------------------------------------------------------------------------------
1 | # 自定义后端
2 |
3 | # 渲染后端
4 |
5 | glass-easel 不仅可以用于 DOM 环境下,它还可以支持其他的 **渲染后端** 。
6 |
7 | glass-easel 可以将它生成的节点树传递给渲染后端。最常见的渲染后端就是 DOM : glass-easel 可以将节点树转换为一些 DOM 调用(如 `document.createElement` 等),然后在浏览器的页面上展示出来。除此之外,在非浏览器环境下,也可以自行实现一些其他渲染后端,接收 glass-easel 生成的节点树、展示界面。
8 |
9 | 渲染后端必须实现 [自定义后端协议](../appendix/backend_protocol.md) ,这个协议有三种模式,支持其中任意一种即可。
10 |
11 | | 协议模式 | 主 TypeScript 接口 | 说明 |
12 | | -------- | --------------- | ---- |
13 | | Composed Mode | `glassEasel.composedBackend.Context` | 首选的协议,相对简单易用 |
14 | | Shadow Mode | `glassEasel.backend.Context` | 针对 Shadow Tree 的协议,整体性能通常是最优的,但是协议本身比较复杂 |
15 | | DOM-like Mode | `glassEasel.domlikeBackend.Context` | 适用于 DOM 的协议,通常只应该用于适配 DOM 接口 |
16 |
17 | 实现渲染后端时,需要实现对应的 TypeScript 接口,例如:
18 |
19 | ```ts
20 | // 一个渲染后端实现
21 | class MyCustomBackend implements glassEasel.composedBackend.Context {
22 | // ...
23 | }
24 | ```
25 |
26 | # 使用自定义的渲染后端
27 |
28 | 要使用一个渲染后端,需要在根组件创建时传入对应的 `Context` 对象:
29 |
30 | ```js
31 | // 创建后端实例
32 | const myCustomBackend = new MyCustomBackend()
33 |
34 | // 连接事件系统
35 | backendContext.onEvent((target, type, detail, options) => {
36 | const ev = new glassEasel.Event(type, detail, options)
37 | glassEasel.Event.dispatchEvent(target, ev)
38 | return ev.defaultPrevented()
39 | ? glassEasel.EventBubbleStatus.NoDefault
40 | : glassEasel.EventBubbleStatus.Normal
41 | })
42 |
43 | // 创建根组件实例
44 | const rootComponent = glassEasel.Component.createWithContext('body', helloWorld, myCustomBackend)
45 |
46 | // 将组件插入到渲染后端的节点树中
47 | const rootNode = myCustomBackend.getRootNode()
48 | const placeholder = myCustomBackend.createElement('placeholder')
49 | rootNode.appendChild(placeholder)
50 | glassEasel.Element.replaceDocumentElement(rootComponent, rootNode, placeholder)
51 | placeholder.release()
52 | rootNode.release()
53 | ```
54 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/tree/mutation_observer.md:
--------------------------------------------------------------------------------
1 | # 节点树变化监听
2 |
3 | Shadow Tree 的变化可以通过 `MutationObserver` 来监听。
4 |
5 | 例如,获取整个 Shadow Tree 上子节点的变化:
6 |
7 | ```js
8 | export const myComponent = componentSpace.defineComponent({
9 | lifetimes: {
10 | attached() {
11 | // 监听 Shadow Tree 上的所有属性变化
12 | glassEasel.MutationObserver.create((ev) => {
13 | ev.type === 'properties' // true
14 | ev.target // 是哪个节点发生了变化
15 | // 如果是组件的属性变化,则 propertyName 会被设置
16 | ev.propertyName // 变化的属性名
17 | // 如果是节点的 id 、 slot 、 class 或 style 变化,则 attributeName 会被设置
18 | ev.attributeName // 变化的项目名
19 | }).observe(this.shadowRoot, { subtree: true, properties: true })
20 |
21 | // 监听 Shadow Tree 上节点的插入、移动或移除
22 | glassEasel.MutationObserver.create((ev) => {
23 | ev.type === 'childList' // true
24 | ev.target // 是哪个节点的子节点列表发生了变化
25 | ev.addedNodes // 新增的节点列表
26 | ev.removedNodes // 移除的节点列表
27 | }).observe(this.shadowRoot, { subtree: true, childList: true })
28 |
29 | // 监听 Shadow Tree 上文本节点的内容变化
30 | glassEasel.MutationObserver.create((ev) => {
31 | ev.type === 'characterData' // true
32 | ev.target // 是哪个节点所含文本发生了变化
33 | }).observe(this.shadowRoot, { subtree: true, characterData: true })
34 |
35 | // 监听当前节点的 attach 和 detach 状态变化
36 | glassEasel.MutationObserver.create((ev) => {
37 | ev.type === 'attachStatus' // true
38 | ev.status // 'attached' 或 'detached'
39 | }).observe(this.shadowRoot, { attachStatus: true })
40 | },
41 | },
42 | })
43 | ```
44 |
45 | 目前 glass-easel 支持的监听:
46 |
47 | * `properties` 属性变化;
48 | * `childList` 子节点列表变化;
49 | * `characterData` 文本内容变化;
50 | * `attachStatus` attach 和 detach 状态变化。
51 |
52 | 一个监听器可以同时监听多类变化。同时可以激活 `subtree: true` 来监听所有子孙节点的变化。( `subtree: true` 对 `attachStatus` 无效。)
53 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/src/escape.rs:
--------------------------------------------------------------------------------
1 | //! Helpers for escaping
2 |
3 | use compact_str::CompactString;
4 | use regex::{Captures, Regex};
5 | use std::borrow::Cow;
6 |
7 | pub(crate) fn escape_html_body(s: &str) -> Cow<'_, str> {
8 | lazy_static! {
9 | static ref REGEX: Regex = Regex::new("[<\"&]").unwrap();
10 | }
11 | REGEX.replace_all(s, |caps: &Captures| match &caps[0] {
12 | "<" => "<".to_owned(),
13 | "\"" => """.to_owned(),
14 | "&" => "&".to_owned(),
15 | _ => unreachable!(),
16 | })
17 | }
18 |
19 | pub(crate) fn escape_html_quote(s: &str) -> Cow<'_, str> {
20 | lazy_static! {
21 | static ref REGEX: Regex = Regex::new("[\"&]").unwrap();
22 | }
23 | REGEX.replace_all(s, |caps: &Captures| match &caps[0] {
24 | "\"" => """.to_owned(),
25 | "&" => "&".to_owned(),
26 | _ => unreachable!(),
27 | })
28 | }
29 |
30 | pub(crate) fn gen_lit_str(s: &str) -> String {
31 | format!("{:?}", s)
32 | }
33 |
34 | pub(crate) fn dash_to_camel(s: &str) -> CompactString {
35 | let mut camel_name = CompactString::new("");
36 | let mut next_upper = false;
37 | for c in s.chars() {
38 | if c == '-' {
39 | next_upper = true;
40 | } else if next_upper {
41 | next_upper = false;
42 | camel_name.push(c.to_ascii_uppercase());
43 | } else {
44 | camel_name.push(c);
45 | }
46 | }
47 | camel_name
48 | }
49 |
50 | pub(crate) fn camel_to_dash(s: &str) -> CompactString {
51 | let mut dash_name = CompactString::new("");
52 | for c in s.chars() {
53 | if c.is_ascii_uppercase() {
54 | dash_name.push('-');
55 | dash_name.push(c.to_ascii_lowercase());
56 | } else {
57 | dash_name.push(c);
58 | }
59 | }
60 | dash_name
61 | }
62 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/index.md:
--------------------------------------------------------------------------------
1 | # glass-easel
2 |
3 | 多后端组件化界面框架
4 |
5 | ## 目录
6 |
7 | ### 基础特性
8 |
9 | [入门](basic/beginning.md)
10 |
11 | [模板](basic/template.md)
12 |
13 | [组件](basic/component.md)
14 |
15 | [生命周期](basic/lifetime.md)
16 |
17 | [方法](basic/method.md)
18 |
19 | [事件](basic/event.md)
20 |
21 | ### 组件间交互
22 |
23 | [slot 及 slot 类型](interaction/slot.md)
24 |
25 | [普通 Behaviors](interaction/behavior.md)
26 |
27 | [Trait Behaviors](interaction/trait_behavior.md)
28 |
29 | [组件路径](interaction/component_path.md)
30 |
31 | [组件间关系](interaction/relation.md)
32 |
33 | [占位组件](interaction/placeholder.md)
34 |
35 | [抽象节点](interaction/generic.md)
36 |
37 | [模板引用](interaction/template_import.md)
38 |
39 | ### 数据控制
40 |
41 | [高级数据更新方法](data_management/advanced_update.md)
42 |
43 | [数据监听器](data_management/data_observer.md)
44 |
45 | [数据字段拷贝控制](data_management/data_deep_copy.md)
46 |
47 | [组件初始化策略](data_management/property_early_init.md)
48 |
49 | [纯数据字段](data_management/pure_data_pattern.md)
50 |
51 | ### 样式控制
52 |
53 | [样式隔离](styling/style_isolation.md)
54 |
55 | [外部样式类](styling/external_class.md)
56 |
57 | [虚拟组件节点](styling/virtual_host.md)
58 |
59 | ### 节点树访问
60 |
61 | [节点树与节点类型](tree/node_tree.md)
62 |
63 | [节点树遍历](tree/element_iterator.md)
64 |
65 | [选择器查询](tree/selector.md)
66 |
67 | [节点树变化监听](tree/mutation_observer.md)
68 |
69 | [节点树变更](tree/node_tree_modification.md)
70 |
71 | ### 高级特性
72 |
73 | [组件空间](advanced/component_space.md)
74 |
75 | [组件构造器中间件](advanced/component_filter.md)
76 |
77 | [绑定映射表更新](advanced/binding_map_update.md)
78 |
79 | [自定义后端](advanced/custom_backend.md)
80 |
81 | [自定义模板引擎](advanced/template_engine.md)
82 |
83 | [外部组件](advanced/external_component.md)
84 |
85 | [警告与错误](advanced/error_listener.md)
86 |
87 | [构建参数](advanced/build_args.md)
88 |
89 | ### 附录
90 |
91 | [自定义后端协议](appendix/backend_protocol.md)
92 |
93 | [list-diff 算法介绍](appendix/list_diff_algorithm.md)
94 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/basic/lifetime.md:
--------------------------------------------------------------------------------
1 | # 生命周期
2 |
3 | ## 普通生命周期
4 |
5 | 组件可以定义一些生命周期回调函数。最常见的是 `attached` 生命周期,它在组件被添加到页面内后立刻触发,例如:
6 |
7 | ```js
8 | // 使用 Definition API 添加生命周期回调函数
9 | export const myComponent = componentSpace.defineComponent({
10 | lifetimes: {
11 | attached() {
12 | // 组件被添加到页面后触发
13 | },
14 | },
15 | })
16 | // 或使用 Chaining API 添加生命周期回调函数
17 | export const myComponent = componentSpace.define()
18 | .lifetime('attached', function () {
19 | // 组件被添加到页面后触发
20 | })
21 | .registerComponent()
22 | // 或在 init 中添加生命周期回调函数
23 | export const myComponent = componentSpace.define()
24 | .init(function ({ lifetime }) {
25 | lifetime('attached', function () {
26 | // 组件被添加到页面后触发
27 | })
28 | })
29 | .registerComponent()
30 | ```
31 |
32 | glass-easel 会自动触发的生命周期列表如下。
33 |
34 | | 生命周期 | 触发时机 | 触发次数 |
35 | | -------- | -------- | ------ |
36 | | `created` | 组件实例刚刚被创建完时触发 | 每个实例触发一次 |
37 | | `attached` | 组件实例被添加到页面后触发 | 每个实例最多触发一次 |
38 | | `moved` | 组件实例在节点树中位置被移动后触发 | 只有 `wx:for` 内项目可能触发,次数不定 |
39 | | `detached` | 组件实例被从页面内移除后触发 | 每个实例最多触发一次 |
40 |
41 | 也可以使用组件实例的 `this.triggerLifetime` 方法来触发生命周期。
42 |
43 | ## 页面生命周期
44 |
45 | 页面生命周期是一类特殊的生命周期。 glass-easel 不会主动触发页面生命周期,只能通过调用组件实例的 `this.triggerPageLifetime` 来触发。
46 |
47 | 触发时,页面生命周期会自动递归到所有子孙组件上。因而它主要用于广播一些全局事件。
48 |
49 | ```js
50 | // 使用 Definition API 添加页面生命周期回调函数
51 | export const myComponent = componentSpace.defineComponent({
52 | pageLifetimes: {
53 | someLifetime() { /* ... */ },
54 | },
55 | })
56 | // 或使用 Chaining API 添加页面生命周期回调函数
57 | export const myComponent = componentSpace.define()
58 | .pageLifetime('someLifetime', function () { /* ... */ })
59 | .registerComponent()
60 | // 或在 init 中添加页面生命周期回调函数
61 | export const myComponent = componentSpace.define()
62 | .init(function ({ pageLifetime }) {
63 | pageLifetime('someLifetime', function () { /* ... */ })
64 | })
65 | .registerComponent()
66 | ```
67 |
--------------------------------------------------------------------------------
/glass-easel/tests/core/class_list.test.ts:
--------------------------------------------------------------------------------
1 | import { domBackend, composedBackend, shadowBackend } from '../base/env'
2 | import * as glassEasel from '../../src'
3 |
4 | const domHtml = (elem: glassEasel.Element): string => {
5 | const domElem = elem.getBackendElement() as unknown as Element
6 | return domElem.outerHTML
7 | }
8 |
9 | const testCases = (testBackend: glassEasel.GeneralBackendContext) => {
10 | const componentSpace = new glassEasel.ComponentSpace()
11 | componentSpace.updateComponentOptions({
12 | writeFieldsToNode: true,
13 | writeIdToDOM: true,
14 | })
15 | componentSpace.defineComponent({
16 | is: '',
17 | })
18 |
19 | it('duplicated class names', () => {
20 | const element = componentSpace.createComponentByUrl('root', '', {}, testBackend)
21 | element.setNodeClass('foo bar foo')
22 | expect(element.class).toBe('foo bar foo')
23 | expect(domHtml(element)).toBe('')
24 | element.classList!.toggle('foo', false)
25 | expect(element.class).toBe('bar foo')
26 | expect(domHtml(element)).toBe('')
27 | element.classList!.toggle('foo', false)
28 | expect(element.class).toBe('bar')
29 | expect(domHtml(element)).toBe('')
30 | element.classList!.toggle('foo', true)
31 | expect(element.class).toBe('bar foo')
32 | expect(domHtml(element)).toBe('')
33 | element.class = 'foo bar foo'
34 | expect(element.class).toBe('foo bar foo')
35 | expect(domHtml(element)).toBe('')
36 | element.class = 'foo bar'
37 | expect(element.class).toBe('foo bar')
38 | expect(domHtml(element)).toBe('')
39 | })
40 | }
41 |
42 | describe('classList (DOM backend)', () => testCases(domBackend))
43 | describe('classList (shadow backend)', () => testCases(shadowBackend))
44 | describe('classList (composed backend)', () => testCases(composedBackend))
45 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/advanced/external_component.md:
--------------------------------------------------------------------------------
1 | # 外部组件
2 |
3 | ## 使用外部组件来提升性能
4 |
5 | 有些时候,出于性能或一些特殊原因,可以使一个组件内部的实现不通过 Shadow Tree 维护,而是直接通过 DOM 接口或 [自定义后端](custom_backend.md) 接口来维护。
6 |
7 | 在组件中添加 `externalComponent` 选项,就可以使它成为一个外部组件,例如:
8 |
9 | ```js
10 | componentSpace.defineComponent({
11 | options: {
12 | externalComponent: true,
13 | },
14 | })
15 | ```
16 |
17 | 这种模式下的组件通常具有很好的性能,但部分特性不可用:
18 |
19 | * 模板中的 `wx:if` `wx:for` 不可用,仅支持 [虚拟树更新](binding_map_update.md) ;
20 | * 由于没有 Shadow Tree ,所有与之相关的树遍历方式(如选择器查询)都不能访问到组件内的节点;
21 | * 大部分 DOM 未提供的对应特性不可用,如外部样式类、复杂事件监听器等。
22 |
23 | ## 外部组件与自定义模板引擎
24 |
25 | 外部组件也可以与 [自定义模板引擎](template_engine.md) 一起使用,这样就可以使得部分 DOM 节点完全脱离 glass-easel 的维护。当需要其他第三方框架来维护部分 DOM 节点时,这个方式很实用。
26 |
27 | 此时,自定义模板引擎在实现 `TemplateInstance` 接口时,返回的 `shadowRoot` 必须是 `glassEasel.ExternalShadowRoot` 类型的,例如:
28 |
29 | ```js
30 | class MyTemplateEngine {
31 | create(rootBehavior, options) {
32 | behavior.getTemplate() // 组件的 template 字段内容
33 | // 检测组件是不是被定义为外部组件
34 | if (!options.externalComponent) {
35 | throw new Error('The template engine can only be used in external component')
36 | }
37 | return new MyTemplate()
38 | }
39 | }
40 |
41 | class MyTemplate {
42 | createInstance(component) {
43 | return new MyTemplateInstance(component)
44 | }
45 | }
46 |
47 | class MyTemplateInstance {
48 | constructor(component) {
49 | // 以外部组件形式使用
50 | this.shadowRoot = {
51 | root: document.body, // 用作 shadowRoot 的节点
52 | slot: document.createElement('span'), // 用作 slot 的节点
53 | getIdMap() {
54 | // 返回节点 id 到节点的映射表
55 | },
56 | handleEvent(target, event) {
57 | // 触发一个事件
58 | },
59 | setListener(element, event, listener) {
60 | // 添加一个事件监听器
61 | },
62 | }
63 | }
64 |
65 | initValues(data) {
66 | // 组件被创建,初始数据是 data
67 | }
68 |
69 | updateValues(data, changes) {
70 | // 组件更新,新的数据是 data ,变更内容描述在 changes 中
71 | }
72 | }
73 | ```
74 |
--------------------------------------------------------------------------------
/glass-easel/src/data_utils.ts:
--------------------------------------------------------------------------------
1 | const deepCopyWithRecursion = (src: T, visited: WeakMap): T => {
2 | if (typeof src === 'object' && src !== null) {
3 | const v = visited.get(src)
4 | if (v !== undefined) return v as T
5 | if (Array.isArray(src)) {
6 | const dest: unknown[] = []
7 | visited.set(src, dest)
8 | for (let i = 0; i < src.length; i += 1) {
9 | dest[i] = deepCopyWithRecursion(src[i], visited)
10 | }
11 | return dest as unknown as T
12 | }
13 | const dest: { [key: string]: unknown } = {}
14 | visited.set(src, dest)
15 | const keys = Object.keys(src)
16 | for (let i = 0; i < keys.length; i += 1) {
17 | const k = keys[i]!
18 | dest[k] = deepCopyWithRecursion((src as { [key: string]: unknown })[k], visited)
19 | }
20 | return dest as T
21 | }
22 | if (typeof src === 'symbol') return Symbol(src.description) as unknown as T
23 | return src
24 | }
25 |
26 | export const simpleDeepCopy = (src: T): T => {
27 | if (typeof src === 'object' && src !== null) {
28 | if (Array.isArray(src)) {
29 | const dest: unknown[] = []
30 | for (let i = 0; i < src.length; i += 1) {
31 | dest[i] = simpleDeepCopy(src[i])
32 | }
33 | return dest as unknown as T
34 | }
35 | const dest: { [key: string]: unknown } = {}
36 | const keys = Object.keys(src)
37 | for (let i = 0; i < keys.length; i += 1) {
38 | const k = keys[i]!
39 | dest[k] = simpleDeepCopy((src as { [key: string]: unknown })[k])
40 | }
41 | return dest as T
42 | }
43 | if (typeof src === 'symbol') return Symbol((src as unknown as symbol).description) as unknown as T
44 | return src
45 | }
46 |
47 | export const deepCopy = (src: T, withRecursion: boolean): T => {
48 | if (withRecursion) return deepCopyWithRecursion(src, new WeakMap())
49 | return simpleDeepCopy(src)
50 | }
51 |
52 | export const enum AutoDestroyState {
53 | Disabled,
54 | Enabled,
55 | Destroyed,
56 | }
57 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-webpack-plugin/wxss_loader.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable */
2 |
3 | const chalk = require('chalk')
4 | const { SourceMapGenerator, SourceMapConsumer } = require('source-map')
5 | const { StyleSheetTransformer } = require('glass-easel-stylesheet-compiler')
6 |
7 | module.exports = function (src, prevMap, meta) {
8 | const callback = this.async()
9 | const { classPrefix, setLowPriorityStyles } = this.query
10 | const sst = new StyleSheetTransformer(this.resourcePath, src, classPrefix, 750, true)
11 | setLowPriorityStyles(sst.getLowPriorityContent(), sst.getLowPrioritySourceMap())
12 | const warnings = sst.extractWarnings()
13 | if (warnings && warnings.length > 0) {
14 | warnings.forEach((warning) => {
15 | const msgKindColored = warning.isError
16 | ? chalk.red('ERROR')
17 | : chalk.yellow('WARN')
18 | const msg = `[glass-easel-stylesheet-compiler] ${msgKindColored} ${warning.path}:${warning.startLine}:${warning.startColumn} (#${warning.code}): ${warning.message}`
19 | if (warning.isError) console.error(msg)
20 | else console.warn(msg)
21 | })
22 | }
23 | const ss = sst.getContent()
24 | let map
25 | if (this.sourceMap) {
26 | const ssSourceMap = JSON.parse(sst.getSourceMap())
27 | sst.free()
28 | if (prevMap) {
29 | const destConsumer = new SourceMapConsumer(ssSourceMap)
30 | const srcConsumer = new SourceMapConsumer(prevMap)
31 | Promise.all([destConsumer, srcConsumer])
32 | .then(([destConsumer, srcConsumer]) => {
33 | const gen = SourceMapGenerator.fromSourceMap(destConsumer)
34 | gen.applySourceMap(srcConsumer, this.resourcePath)
35 | destConsumer.destroy()
36 | srcConsumer.destroy()
37 | map = gen.toJSON()
38 | callback(null, ss, map, meta)
39 | return undefined
40 | })
41 | .catch((err) => {
42 | callback(err)
43 | })
44 | } else {
45 | map = ssSourceMap
46 | callback(null, ss, map, meta)
47 | }
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/glass-easel/src/virtual_node.ts:
--------------------------------------------------------------------------------
1 | import { BM, BackendMode, type GeneralBackendContext } from './backend'
2 | import { performanceMeasureEnd, performanceMeasureStart } from './dev_tools'
3 | import { Element } from './element'
4 | import { ENV } from './global_options'
5 | import { type ShadowRoot } from './shadow_root'
6 | import { VIRTUAL_NODE_SYMBOL, isVirtualNode } from './type_symbol'
7 |
8 | export class VirtualNode extends Element {
9 | [VIRTUAL_NODE_SYMBOL]: true
10 | is: string
11 |
12 | /* @internal */
13 | /* istanbul ignore next */
14 | constructor() {
15 | throw new Error('Element cannot be constructed directly')
16 | // eslint-disable-next-line no-unreachable
17 | super()
18 | }
19 |
20 | /* @internal */
21 | protected _$initializeVirtual(
22 | virtualName: string,
23 | owner: ShadowRoot,
24 | nodeTreeContext: GeneralBackendContext | null,
25 | ) {
26 | this.is = String(virtualName)
27 | if (
28 | nodeTreeContext &&
29 | (BM.SHADOW || (BM.DYNAMIC && nodeTreeContext.mode === BackendMode.Shadow))
30 | ) {
31 | const shadowRoot = owner._$backendShadowRoot!
32 | if (ENV.DEV) performanceMeasureStart('backend.createVirtualNode')
33 | const be = shadowRoot.createVirtualNode(virtualName)
34 | if (ENV.DEV) performanceMeasureEnd()
35 | this._$initialize(true, be, owner, nodeTreeContext)
36 | if (ENV.DEV) performanceMeasureStart('backend.associateValue')
37 | be.__wxElement = this
38 | be.associateValue(this)
39 | if (ENV.DEV) performanceMeasureEnd()
40 | } else {
41 | this._$initialize(true, null, owner, owner._$nodeTreeContext)
42 | }
43 | }
44 |
45 | static isVirtualNode = isVirtualNode
46 |
47 | /* @internal */
48 | static create(virtualName: string, owner: ShadowRoot): VirtualNode {
49 | const node = Object.create(VirtualNode.prototype) as VirtualNode
50 | node._$initializeVirtual(virtualName, owner, owner.getBackendContext())
51 | return node
52 | }
53 | }
54 |
55 | VirtualNode.prototype[VIRTUAL_NODE_SYMBOL] = true
56 |
--------------------------------------------------------------------------------
/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
57 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/tree/selector.md:
--------------------------------------------------------------------------------
1 | # 选择器查询
2 |
3 | ## 使用选择器获取节点
4 |
5 | 通常,可通过选择器来快速获得 Shadow Tree 上一个特定的子孙节点。
6 |
7 | 例如,获得一个 id 为 `the-id` 的子节点:
8 |
9 | ```js
10 | export const myComponent = componentSpace.defineComponent({
11 | template: compileTemplate(`
12 |
13 | Some Text
14 |
15 | `),
16 | lifetimes: {
17 | attached() {
18 | // 获取 id="the-id" 的节点
19 | const span = this.shadowRoot.querySelector('#the-id')
20 | span.childNodes[0].textContent = 'Some Text'
21 | },
22 | },
23 | })
24 | ```
25 |
26 | 目前 glass-easel 支持的选择器列表如下。
27 |
28 | | 名称 | 示例 | 说明 |
29 | | ---- | ---- | ---- |
30 | | id 选择器 | `#the-id` | 选择节点 id 为 `the-id` 的节点 |
31 | | class 选择器 | `.the-class` | 选择节点 class 含 `the-class` 的节点 |
32 | | 交集选择 | `#the-id.the-class` | 选择节点 id 为 `the-id` 且 class 含 `the-class` 的节点 |
33 | | 儿子选择 | `#the-id > .the-class` | 在节点 id 为 `the-id` 节点的儿子节点中,选择节点 class 含 `the-class` 的节点 |
34 | | 子孙选择 | `#the-id .the-class` | 在节点 id 为 `the-id` 节点的子孙节点中,选择节点 class 含 `the-class` 的节点 |
35 | | 跨组件子孙选择 | `#the-id >>> .the-class` | 在节点 id 为 `the-id` 节点的子孙节点中,选择节点 class 含 `the-class` 的节点,查找时可以进入其他组件、不限于当前 Shadow Tree |
36 |
37 | `querySelector` 只会返回第一个找到的节点。如果需要返回所有节点组成的列表,使用 `querySelectorAll` 。
38 |
39 | ## 节点类型特化
40 |
41 | 在使用 TypeScript 时,选择器查询返回的节点都是 `glassEasel.Element` 类型。此时可以使用一些类型特化方法将其转换为更具体的节点类型。
42 |
43 | 最常用的类型特化方法是 `asInstanceOf` ,用来将一个节点转为某个特定组件的实例,例如:
44 |
45 | ```js
46 | export const childComponent = componentSpace.define()
47 | .methods({
48 | aPlusB(a, b) { return a + b }
49 | })
50 | .registerComponent()
51 | export const myComponent = componentSpace.define()
52 | .usingComponents({
53 | child: childComponent,
54 | })
55 | .template(compileTemplate(`
56 |
57 | `)
58 | .lifetime('attached', function () {
59 | // 获取 id="the-id" 的节点,并将其转换为 childComponent 的实例
60 | const child = this.shadowRoot.querySelector('#the-id').asInstanceOf(childComponent)!
61 | child.aPlusB(1, 2) === 3 // true
62 | })
63 | .registerComponent()
64 | ```
65 |
66 | 另一种比较好的实现方式是不关心节点的具体类型、只关心节点实现的接口,以实现更好的组件间解耦,此时可以使用 [Trait Behaviors](../interaction/trait_behavior.md) 。
67 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build-and-test
2 | on:
3 | push:
4 | branches: ["master"]
5 | pull_request:
6 | branches:
7 | - master
8 |
9 | jobs:
10 | build-and-test:
11 | strategy:
12 | matrix:
13 | node-version: [lts/*, latest]
14 | os: ['windows-latest', 'ubuntu-latest']
15 | fail-fast: false
16 | name: Build and Test
17 | runs-on: ${{ matrix.os }}
18 | steps:
19 | - name: Checkout master
20 | uses: actions/checkout@v3
21 | - name: Setup pnpm
22 | uses: pnpm/action-setup@v2
23 | with:
24 | version: latest
25 | - name: Setup node ${{ matrix.node-version }}
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | - name: Install
30 | uses: pnpm/action-setup@v2
31 | with:
32 | version: 9
33 | run_install: |
34 | - recursive: true
35 | args: [--frozen-lockfile, --strict-peer-dependencies]
36 | - name: Setup Rust and Cargo
37 | uses: dtolnay/rust-toolchain@stable
38 | with:
39 | toolchain: stable
40 | targets: wasm32-unknown-unknown
41 | - name: Setup wasm-pack
42 | uses: jetli/wasm-pack-action@v0.4.0
43 | with:
44 | version: 'v0.13.1'
45 | - name: Build
46 | run: |
47 | pnpm -r run build
48 | - name: Test glass-easel
49 | working-directory: glass-easel
50 | run: |
51 | pnpm run lint
52 | pnpm run test
53 | - name: Test glass-easel-miniprogram-adapter
54 | working-directory: glass-easel-miniprogram-adapter
55 | run: |
56 | pnpm run lint
57 | pnpm run test
58 | - name: Test glass-easel-shadow-sync
59 | working-directory: glass-easel-shadow-sync
60 | run: |
61 | pnpm run test
62 | - name: Test glass-easel-stylesheet-compiler
63 | working-directory: glass-easel-stylesheet-compiler
64 | run: |
65 | cargo test
66 | - name: Test glass-easel-template-compiler
67 | working-directory: glass-easel-template-compiler
68 | run: |
69 | cargo test
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/rollup.config.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-extraneous-dependencies */
2 | import nodeResolve from '@rollup/plugin-node-resolve'
3 | import terser from '@rollup/plugin-terser'
4 | import typescript from '@rollup/plugin-typescript'
5 | import type { RollupOptions } from 'rollup'
6 | import dts from 'rollup-plugin-dts'
7 |
8 | const jobs: string[] = []
9 | let minimize = true
10 | let sourcemap = true
11 | const args = (process.env as { GLASS_EASEL_ARGS?: string }).GLASS_EASEL_ARGS || ''
12 | args.split(' ').forEach((arg) => {
13 | if (!arg) return
14 | if (arg[0] === '-') {
15 | if (arg === '--no-minimize') minimize = false
16 | else if (arg === '--dev') {
17 | minimize = false
18 | sourcemap = false
19 | }
20 | } else {
21 | jobs.push(arg)
22 | }
23 | })
24 |
25 | const config: RollupOptions[] = [
26 | {
27 | input: './src/index.ts',
28 | external: ['glass-easel'],
29 | output: {
30 | file: 'dist/glass_easel_miniprogram_adapter.js',
31 | sourcemap,
32 | format: 'cjs',
33 | },
34 | plugins: [
35 | nodeResolve({
36 | extensions: ['.ts', 'js'],
37 | }),
38 | typescript({
39 | sourceMap: sourcemap,
40 | }),
41 | minimize
42 | ? terser({
43 | sourceMap: sourcemap,
44 | })
45 | : null,
46 | ],
47 | },
48 | {
49 | input: './src/index.ts',
50 | external: ['glass-easel'],
51 | output: {
52 | file: 'dist/glass_easel_miniprogram_adapter.es.js',
53 | sourcemap,
54 | format: 'es',
55 | },
56 | plugins: [
57 | nodeResolve({
58 | extensions: ['.ts', 'js'],
59 | }),
60 | typescript({
61 | sourceMap: sourcemap,
62 | }),
63 | minimize
64 | ? terser({
65 | sourceMap: sourcemap,
66 | })
67 | : null,
68 | ],
69 | },
70 | {
71 | input: './src/index.ts',
72 | external: ['glass-easel'],
73 | output: { file: `dist/glass_easel_miniprogram_adapter.d.ts`, format: 'es' },
74 | plugins: [
75 | nodeResolve({
76 | extensions: ['.ts', 'js'],
77 | }),
78 | dts(),
79 | ],
80 | },
81 | ]
82 |
83 | export default config
84 |
--------------------------------------------------------------------------------
/glass-easel/src/template_engine.ts:
--------------------------------------------------------------------------------
1 | import { type GeneralBehavior } from './behavior'
2 | import { type ShadowRoot } from './shadow_root'
3 | import { type DataChange, type DataValue } from './data_proxy'
4 | import { type NormalizedComponentOptions } from './global_options'
5 | import { type ExternalShadowRoot } from './external_shadow_tree'
6 | import { type GeneralComponent } from './component'
7 |
8 | /**
9 | * A template engine that handles the template part of a component
10 | */
11 | export interface TemplateEngine {
12 | /**
13 | * Preprocess a behavior and generate a preprocessed template
14 | *
15 | * This function is called during component prepare.
16 | * The `_$template` field of the behavior is designed to be handled by the template engine,
17 | * and should be preprocessed in this function.
18 | */
19 | create(behavior: GeneralBehavior, componentOptions: NormalizedComponentOptions): Template
20 | }
21 |
22 | /**
23 | * A preprocessed template
24 | */
25 | export interface Template {
26 | /**
27 | * Create a template instance for a component instance
28 | */
29 | createInstance(
30 | elem: GeneralComponent,
31 | createShadowRoot: (component: GeneralComponent) => ShadowRoot,
32 | ): TemplateInstance
33 |
34 | /**
35 | * Update the content of the template (optional)
36 | *
37 | * Implement this function if template update is needed (usually used during development).
38 | * The behavior is always the object which used when creation.
39 | */
40 | updateTemplate?(behavior: GeneralBehavior): void
41 | }
42 |
43 | /**
44 | * A template instance that works with a component instance
45 | */
46 | export interface TemplateInstance {
47 | /**
48 | * The shadow root of the component
49 | *
50 | * This field should not be changed.
51 | */
52 | shadowRoot: ShadowRoot | ExternalShadowRoot
53 |
54 | /**
55 | * Apply the updated template content (optional)
56 | *
57 | * Implement this function if template update is needed (usually used during development).
58 | * The template is always the object which used when creation.
59 | */
60 | updateTemplate?(template: Template, data: DataValue): void
61 |
62 | initValues(data: DataValue): void
63 |
64 | updateValues(data: DataValue, changes: DataChange[]): void
65 | }
66 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/basic/beginning.md:
--------------------------------------------------------------------------------
1 | # 入门
2 |
3 | glass-easel 将整个页面视为由 **组件** 组成的,组件间可以相互引用。
4 |
5 | 其中一个组件用于展示完整页面内容,称为 **根组件** 。在一个最简单的页面中,可以只有一个根组件。
6 |
7 | 每个组件大体上由三部分内容组成:模板、样式、脚本。
8 |
9 | ## 模板及其编译
10 |
11 | 模板就是包含 `{{ ... }}` 数据绑定的类 XML 代码。 glass-easel 模板遵循 WXML 语法。例如:
12 |
13 | ```xml
14 |
15 | {{ hello }}
16 |
17 | ```
18 |
19 | 模板需要预先使用 `glass-easel-template-compiler` 编译。这个编译器既可以通过 WebAssembly 的方式引入,也可以在构建期间调用。
20 |
21 | 如果是以 WebAssembly 方式引入,基本的调用方法是:
22 |
23 | ```js
24 | import { TmplGroup } from 'glass-easel-template-compiler'
25 | const compileTemplate = (src: string) => {
26 | const group = new TmplGroup()
27 | group.addTmpl('', src)
28 | const genObjectSrc = `return ${group.getTmplGenObjectGroups()}`
29 | group.free()
30 | const genObjectGroupList = (new Function(genObjectSrc))() as { [key: string]: any }
31 | return {
32 | content: genObjectGroupList[''],
33 | }
34 | }
35 | ```
36 |
37 | ## 样式
38 |
39 | 样式就是一段 CSS 代码。例如:
40 |
41 | ```css
42 | .blue {
43 | color: blue;
44 | }
45 | ```
46 |
47 | glass-easel 本身并不对 CSS 做过多处理,直接将其引入页面即可。
48 |
49 | ## 脚本
50 |
51 | 组件需要用一段 JavaScript 或 TypeScript 代码来定义出来。例如:
52 |
53 | ```js
54 | // hello-world.js
55 | import * as glassEasel from 'glass-easel'
56 |
57 | // 定义一个组件空间
58 | const componentSpace = new glassEasel.ComponentSpace()
59 |
60 | // 组件模板
61 | const template = `
62 |
63 | {{ hello }}
64 |
65 | `
66 |
67 | // 定义组件
68 | export const helloWorld = componentSpace.defineComponent({
69 | // 组件所使用的模板
70 | template: compileTemplate(template),
71 | // 用在组件模板上的数据
72 | data: {
73 | hello: 'Hello world!'
74 | },
75 | })
76 | ```
77 |
78 | ## 挂载
79 |
80 | 最后,在 DOM 环境下,需要一段启动脚本来将根组件 **挂载** 到页面内。例如:
81 |
82 | ```js
83 | import * as glassEasel from 'glass-easel'
84 | import { helloWorld } from './hello-world'
85 |
86 | // 创建根组件实例
87 | const domBackend = new glassEasel.domlikeBackend.CurrentWindowBackendContext()
88 | const rootComponent = glassEasel.Component.createWithContext('body', helloWorld, domBackend)
89 |
90 | // 将组件插入到 DOM 树中
91 | const placeholder = document.createElement('span')
92 | document.body.appendChild(placeholder)
93 | glassEasel.Element.replaceDocumentElement(rootComponent, document.body, placeholder)
94 | ```
95 |
--------------------------------------------------------------------------------
/.github/workflows/pages.yml:
--------------------------------------------------------------------------------
1 | name: deploy-pages
2 |
3 | on:
4 | push:
5 | branches: ["master"]
6 |
7 | workflow_dispatch:
8 |
9 | permissions:
10 | contents: read
11 | pages: write
12 | id-token: write
13 |
14 | concurrency:
15 | group: "pages"
16 | cancel-in-progress: false
17 |
18 | jobs:
19 | deploy:
20 | environment:
21 | name: github-pages
22 | url: ${{ steps.deployment.outputs.page_url }}
23 | runs-on: ubuntu-latest
24 | steps:
25 | - name: Checkout
26 | uses: actions/checkout@v3
27 | - name: Setup Pages
28 | uses: actions/configure-pages@v3
29 | - name: Setup pnpm
30 | uses: pnpm/action-setup@v2
31 | with:
32 | version: latest
33 | - name: Setup node
34 | uses: actions/setup-node@v3
35 | with:
36 | node-version: 20
37 | - name: Install
38 | uses: pnpm/action-setup@v2
39 | with:
40 | version: 9
41 | run_install: |
42 | - recursive: true
43 | args: [--frozen-lockfile, --strict-peer-dependencies]
44 | - name: Setup Rust and Cargo
45 | uses: dtolnay/rust-toolchain@stable
46 | with:
47 | toolchain: stable
48 | targets: wasm32-unknown-unknown
49 | - name: Setup wasm-pack
50 | uses: jetli/wasm-pack-action@v0.4.0
51 | with:
52 | version: 'v0.10.3'
53 | - name: Build
54 | run: |
55 | pnpm -r run build
56 | - name: Generate docs for glass-easel
57 | working-directory: glass-easel
58 | run: |
59 | npm run doc
60 | - name: Generate docs for glass-easel-miniprogram-adapter
61 | working-directory: glass-easel-miniprogram-adapter
62 | run: |
63 | npm run doc
64 | - name: Collect artifacts
65 | run: |
66 | mkdir github-pages
67 | mkdir github-pages/docs
68 | mv glass-easel/docs github-pages/docs/glass-easel
69 | mv glass-easel-miniprogram-adapter/docs github-pages/docs/glass-easel-miniprogram-adapter
70 | - name: Upload artifact
71 | uses: actions/upload-pages-artifact@v3
72 | with:
73 | path: 'github-pages'
74 | - name: Deploy to GitHub Pages
75 | id: deployment
76 | uses: actions/deploy-pages@v4
77 |
--------------------------------------------------------------------------------
/glass-easel/tests/types/createElement.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-unsafe-member-access */
2 | /* eslint-disable no-param-reassign */
3 | /* eslint-disable @typescript-eslint/ban-ts-comment */
4 | // eslint-disable-next-line import/no-extraneous-dependencies
5 | import { expectType } from 'tsd-lite'
6 | import * as glassEasel from '../../src'
7 |
8 | const componentSpace = glassEasel.getDefaultComponentSpace()
9 |
10 | /**
11 | * Definition createElement
12 | */
13 | const definitionInstance = glassEasel.createElement(
14 | 'comp',
15 | glassEasel.registerElement({
16 | properties: {
17 | propStr: String,
18 | },
19 | data: {
20 | foo: { foo: 'foo' },
21 | },
22 | methods: {
23 | func() {
24 | return this.data.propStr + this.data.foo.foo
25 | },
26 | },
27 | }),
28 | )
29 |
30 | expectType(definitionInstance.data.propStr)
31 | expectType<{ foo: string }>(definitionInstance.data.foo)
32 | expectType(definitionInstance.func())
33 |
34 | const definitionGeneralInstance = glassEasel.createElement(
35 | 'comp',
36 | glassEasel
37 | .registerElement({
38 | properties: {
39 | propStr: String,
40 | },
41 | data: {
42 | foo: { foo: 'foo' },
43 | },
44 | })
45 | .general(),
46 | )
47 |
48 | expectType<{ [x: string]: any }>(definitionGeneralInstance.data)
49 |
50 | /**
51 | * Chaining createElement
52 | */
53 | const chainingInstance = glassEasel.createElement(
54 | 'comp',
55 | componentSpace
56 | .define()
57 | .property('propStr', String)
58 | .data(() => ({
59 | foo: { foo: 'foo' },
60 | }))
61 | .init(({ data, method }) => {
62 | const func = method(() => data.propStr + data.foo.foo)
63 |
64 | return { func }
65 | })
66 | .registerComponent(),
67 | )
68 |
69 | expectType(chainingInstance.data.propStr)
70 | expectType<{ foo: string }>(chainingInstance.data.foo)
71 | expectType(chainingInstance.func())
72 |
73 | const chainingGeneralInstance = glassEasel.createElement(
74 | 'comp',
75 | componentSpace
76 | .define()
77 | .property('propStr', String)
78 | .data(() => ({
79 | foo: { foo: 'foo' },
80 | }))
81 | .registerComponent()
82 | .general(),
83 | )
84 |
85 | expectType<{ [x: string]: any }>(chainingGeneralInstance.data)
86 |
--------------------------------------------------------------------------------
/glass-easel/tests/core/relation.test.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-restricted-syntax */
2 | import { domBackend } from '../base/env'
3 | import * as glassEasel from '../../src'
4 |
5 | const componentSpace = new glassEasel.ComponentSpace()
6 | componentSpace.updateComponentOptions({
7 | writeFieldsToNode: true,
8 | })
9 | componentSpace.defineComponent({
10 | is: '',
11 | })
12 |
13 | describe('Relation', () => {
14 | test('should link when another component is lazily loaded', () => {
15 | const childLinkedList: ['linked' | 'unlinked', glassEasel.GeneralComponent][] = []
16 | const childDef = componentSpace.defineComponent({
17 | is: 'child',
18 | relations: {
19 | parent: {
20 | type: 'ancestor',
21 | linked(target) {
22 | childLinkedList.push(['linked', target])
23 | },
24 | unlinked(target) {
25 | childLinkedList.push(['unlinked', target])
26 | },
27 | },
28 | },
29 | })
30 | const child = glassEasel.Component.createWithContext('child', childDef, domBackend)
31 |
32 | expect(childLinkedList).toEqual([])
33 |
34 | const parentLinkedList: ['linked' | 'unlinked', glassEasel.GeneralComponent][] = []
35 | const parentDef = componentSpace.defineComponent({
36 | is: 'parent',
37 | relations: {
38 | child: {
39 | type: 'descendant',
40 | linked(target) {
41 | parentLinkedList.push(['linked', target])
42 | },
43 | unlinked(target) {
44 | parentLinkedList.push(['unlinked', target])
45 | },
46 | },
47 | },
48 | })
49 | const parent = glassEasel.Component.createWithContext('parent', parentDef, domBackend)
50 | glassEasel.Element.pretendAttached(parent)
51 |
52 | expect(childLinkedList).toStrictEqual([])
53 | expect(parentLinkedList).toStrictEqual([])
54 |
55 | parent.appendChild(child)
56 |
57 | expect(childLinkedList).toStrictEqual([['linked', parent]])
58 | expect(parentLinkedList).toStrictEqual([['linked', child]])
59 |
60 | parent.removeChild(child)
61 |
62 | expect(childLinkedList).toStrictEqual([
63 | ['linked', parent],
64 | ['unlinked', parent],
65 | ])
66 | expect(parentLinkedList).toStrictEqual([
67 | ['linked', child],
68 | ['unlinked', child],
69 | ])
70 | })
71 | })
72 |
--------------------------------------------------------------------------------
/glass-easel/src/backend/composed_backend_protocol.ts:
--------------------------------------------------------------------------------
1 | import { type Element as GlassEaselElement } from '../element'
2 | import { type MutLevel, type EventBubbleStatus, type EventOptions } from '../event'
3 | import { type BackendMode } from './shared'
4 | import type * as suggestedBackend from './suggested_backend_protocol'
5 |
6 | export interface Context extends Partial> {
7 | mode: BackendMode.Composed
8 | destroy(): void
9 | getWindowWidth(): number
10 | getWindowHeight(): number
11 | getDevicePixelRatio(): number
12 | getTheme(): string
13 | registerStyleSheetContent(path: string, content: unknown): void
14 | appendStyleSheetPath(path: string, styleScope?: number): number
15 | disableStyleSheet(index: number): void
16 | render(cb: (err: Error | null) => void): void
17 | getRootNode(): Element
18 | createElement(logicalName: string, stylingName: string): Element
19 | createTextNode(content: string): Element
20 | createFragment(): Element
21 | onEvent(
22 | listener: (
23 | target: GlassEaselElement,
24 | type: string,
25 | detail: unknown,
26 | options: EventOptions,
27 | ) => EventBubbleStatus | void,
28 | ): void
29 | }
30 |
31 | export interface Element extends Partial> {
32 | __wxElement?: GlassEaselElement
33 | release(): void
34 | associateValue(v: GlassEaselElement): void
35 | appendChild(child: Element): void
36 | removeChild(child: Element, index?: number): void
37 | insertBefore(child: Element, before: Element, index?: number): void
38 | replaceChild(child: Element, oldChild: Element, index?: number): void
39 | spliceBefore(before: Element, deleteCount: number, list: Element): void
40 | spliceAppend(list: Element): void
41 | spliceRemove(before: Element, deleteCount: number): void
42 | setId(id: string): void
43 | setStyleScope(styleScope: number, extraStyleScope?: number, hostStyleScope?: number): void
44 | setStyle(styleText: string): void
45 | addClass(elementClass: string, styleScope?: number): void
46 | removeClass(elementClass: string, styleScope?: number): void
47 | clearClasses(): void
48 | setAttribute(name: string, value: unknown): void
49 | removeAttribute(name: string): void
50 | setText(content: string): void
51 | setModelBindingStat(attributeName: string, listener: ((newValue: unknown) => void) | null): void
52 | setListenerStats(type: string, capture: boolean, mutLevel: MutLevel): void
53 | }
54 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/tree/node_tree_modification.md:
--------------------------------------------------------------------------------
1 | # 节点树变更
2 |
3 | ## 何时可以手工变更节点树
4 |
5 | 绝大多数情况下,节点树都应该通过模板和数据绑定来改变。但在极少数情况下,需要绕开模板来手工变更节点树。
6 |
7 | 一种情况是,在实现 [自定义模板引擎](../advanced/template_engine.md) 时需要手工变更节点树。
8 |
9 | 另一种情况是,有些极特殊的组件树结构无法采用模板引擎表达。请注意:这种情况下,对于节点树的手工变更常常会和模板带来的更新冲突!通常,只应该更新不在模板上的节点,例如在模板的一个空节点内部插入新节点。
10 |
11 | 变更节点树时,只需要变更 Shadow Tree ,而 Composed Tree 会自动变更。所以只关心 Shadow Tree 的变更方法即可。
12 |
13 | ## 创建一个节点
14 |
15 | 创建节点时,可以使用组件实例的 `this.shadowRoot` 下的几个创建方法。
16 |
17 | | 方法名 | 说明 |
18 | | ------ | ---- |
19 | | createTextNode | 创建一个 `glassEasel.TextNode` 文本节点 |
20 | | createNativeNode | 创建一个 `glassEasel.NativeNode` 普通节点 |
21 | | createComponent | 创建一个 `glassEasel.Component` 组件节点 |
22 | | createComponentOrNativeNode | 如果节点名字是一个组件,就创建一个 `glassEasel.Component` 组件节点,否则创建一个 `glassEasel.NativeNode` 普通节点 |
23 | | createVirtualNode | 创建一个 `glassEasel.VirtualNode` 虚拟节点 |
24 |
25 | 注意,在一个 `glassEasel.ShadowRoot` 下创建的节点只能被插入到这个 Shadow Tree 中。
26 |
27 | ## 增删节点
28 |
29 | 将节点插入节点树或移出节点树时,可以使用 `glassEasel.Element` 下的几个插入、移除方法。
30 |
31 | | 方法名 | 说明 |
32 | | ------ | ---- |
33 | | appendChild | 追加一个新子节点 |
34 | | insertChildAt | 在第 index 个子节点的位置上插入一个新子节点 |
35 | | insertBefore | 在指定的子节点之前插入一个新子节点 |
36 | | insertChildren | 批量插入新子节点 |
37 | | removeChildAt | 移除第 index 个子节点 |
38 | | removeChild | 移除指定的子节点 |
39 | | removeChildren | 批量移除子节点 |
40 | | replaceChildAt | 替换第 index 个子节点为新子节点 |
41 | | replaceChild | 替换指定的子节点为新子节点 |
42 | | selfReplaceWith | 将节点自身替换为一个新节点 |
43 |
44 | 如果将一个已经被插入的节点插入到另一个位置上,则会自动变为一个移动操作。
45 |
46 | glass-easel 内部使用的算法是针对在一个节点末尾追加新子节点(即 `appendChild` )来优化的,所以请尽量多使用这样的操作。
47 |
48 | ## 销毁节点
49 |
50 | 将一个节点移出节点树之后,如果它不再被使用,则需要调用销毁方法 `glassEasel.Element.destroyBackendElement` ,否则有些情况下会出现内存泄漏。
51 |
52 | 如果一个节点在被移出节点树后就必然不会再被使用,则可以提前调用 `glassEasel.Element.destroyBackendElementOnDetach` 使它在被移出后自动销毁,避免遗忘。
53 |
54 | ## 节点树变更流程示例
55 |
56 | 对节点树的操作流程可以大体上以下例表示:
57 |
58 | ```js
59 | export const myComponent = componentSpace.defineComponent({
60 | template: compileTemplate(`
61 |
62 | `),
63 | lifetimes: {
64 | attached() {
65 | // 获得 wrapper 节点
66 | const wrapper = this.shadowRoot.getElementById('#wrapper')
67 | // 创建一个新的 span 节点
68 | const span = this.shadowRoot.createNativeNode('span')
69 | // 当 span 被移出时自动销毁它
70 | span.destroyBackendElementOnDetach()
71 | // 将 span 插入到 wrapper 下
72 | wrapper.appendChild(span)
73 | // 将 span 移除出来
74 | wrapper.removeChildAt(0)
75 | },
76 | },
77 | })
78 | ```
79 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-template/webpack.config.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 |
3 | const path = require('path')
4 | const {
5 | GlassEaselMiniprogramWebpackPlugin,
6 | GlassEaselMiniprogramWxmlLoader,
7 | GlassEaselMiniprogramWxssLoader,
8 | } = require('glass-easel-miniprogram-webpack-plugin')
9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin')
10 |
11 | module.exports = [
12 | {
13 | mode: 'production',
14 | entry: './src/index.js', // this file is virtually generated by the plugin
15 | output: {
16 | filename: 'index.js',
17 | path: path.join(__dirname, 'dist'),
18 | module: false,
19 | iife: true,
20 | },
21 | devtool: 'source-map',
22 | resolve: {
23 | extensions: ['.ts', '.js'],
24 | alias: {
25 | 'glass-easel': 'glass-easel',
26 | },
27 | },
28 | module: {
29 | rules: [
30 | {
31 | test: /\.ts$/,
32 | loader: 'ts-loader',
33 | exclude: /node_modules/,
34 | },
35 | {
36 | // wxml should be explicit handled with a loader
37 | test: /\.wxml$/,
38 | use: GlassEaselMiniprogramWxmlLoader,
39 | exclude: /node_modules/,
40 | },
41 | {
42 | // wxss should be explicit handled like CSS
43 | test: /\.wxss$/,
44 | use: [
45 | MiniCssExtractPlugin.loader,
46 | 'css-loader',
47 | GlassEaselMiniprogramWxssLoader,
48 | // Add more loaders here if you work with less, sass, Stylus, etc.
49 | // Currently `@import` does not work well without a preprocessor (such as `less`).
50 | // This is a bug (#113) and will be fixed in future.
51 | 'less-loader',
52 | ],
53 | exclude: /node_modules/,
54 | },
55 | ],
56 | },
57 | plugins: [
58 | new MiniCssExtractPlugin({
59 | filename: 'index.css',
60 | }),
61 | new GlassEaselMiniprogramWebpackPlugin({
62 | // the path of the mini-program code directory
63 | path: path.join(__dirname, 'src'),
64 | // the resouce files that should be copied to the dist directory
65 | resourceFilePattern: /\.(jpg|jpeg|png|gif|html)$/,
66 | // the default entry
67 | defaultEntry: 'pages/index/index',
68 | // compile with more debug information (unset to follow the webpack mode)
69 | // dev: false,
70 | }),
71 | ],
72 | },
73 | ]
74 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/tests/spec/replay.test.ts:
--------------------------------------------------------------------------------
1 | import * as glassEasel from 'glass-easel'
2 | import { type ShadowSyncElement } from '../../src/backend'
3 | import {
4 | bridgeOnData,
5 | bridgeOnView,
6 | createViewContext,
7 | domBackend,
8 | shadowSyncBackend,
9 | tmpl,
10 | viewComponentSpace,
11 | } from '../base/env'
12 |
13 | const componentSpace = new glassEasel.ComponentSpace()
14 | componentSpace.updateComponentOptions({
15 | writeFieldsToNode: true,
16 | writeIdToDOM: true,
17 | })
18 |
19 | const replayedDomHtml = (elem: glassEasel.Element): string => {
20 | bridgeOnData.disconnect()
21 | bridgeOnView.disconnect()
22 |
23 | const rootNode = document.createElement('div')
24 | const newViewContext = createViewContext(
25 | rootNode as unknown as glassEasel.domlikeBackend.Element,
26 | domBackend,
27 | viewComponentSpace,
28 | )
29 |
30 | bridgeOnData.connect(newViewContext.bridgeOnView)
31 | shadowSyncBackend.replay(
32 | glassEasel,
33 | [elem],
34 | (elem) => elem.getBackendElement() as ShadowSyncElement,
35 | )
36 | const innerHTML = (rootNode.firstChild! as Element).innerHTML
37 |
38 | bridgeOnData.connect(bridgeOnView)
39 | return innerHTML
40 | }
41 |
42 | describe('replay', () => {
43 | test('basic replay', () => {
44 | const child = componentSpace.defineComponent({
45 | properties: {
46 | text: String,
47 | },
48 | template: tmpl(`
49 | {{text}}
50 | `),
51 | })
52 | const rootDef = componentSpace.defineComponent({
53 | using: { child },
54 | template: tmpl(`
55 | !{{text}}!
56 | `),
57 | data: {
58 | text: '',
59 | },
60 | })
61 | const elem = glassEasel.Component.createWithContext('root', rootDef, shadowSyncBackend)
62 | elem.destroyBackendElementOnDetach()
63 |
64 | expect(replayedDomHtml(elem)).toEqual('')
65 | elem.setData({
66 | text: '123',
67 | })
68 | expect(replayedDomHtml(elem)).toEqual(
69 | '123!123!',
70 | )
71 | elem.setData({
72 | text: '233',
73 | })
74 | expect(replayedDomHtml(elem)).toEqual(
75 | '233!233!',
76 | )
77 | elem.setData({
78 | text: '',
79 | })
80 | expect(replayedDomHtml(elem)).toEqual('')
81 | })
82 | })
83 |
--------------------------------------------------------------------------------
/glass-easel/src/backend/shared.ts:
--------------------------------------------------------------------------------
1 | export const BM = {
2 | DYNAMIC: true,
3 | SHADOW: false,
4 | COMPOSED: false,
5 | DOMLIKE: false,
6 | }
7 |
8 | export const enum BackendMode {
9 | Shadow = 1,
10 | Composed = 2,
11 | Domlike = 3,
12 | }
13 |
14 | export type BoundingClientRect = {
15 | left: number
16 | top: number
17 | width: number
18 | height: number
19 | }
20 |
21 | export type ScrollOffset = {
22 | scrollLeft: number
23 | scrollTop: number
24 | scrollWidth: number
25 | scrollHeight: number
26 | }
27 |
28 | export type CSSProperty = {
29 | name: string
30 | value: string
31 | disabled?: boolean
32 | invalid?: boolean
33 | important?: boolean
34 | }
35 | export type CSSRule = {
36 | sheetIndex: number
37 | ruleIndex: number
38 | inlineText: string
39 | mediaQueries: string[]
40 | selector: string
41 | selectors: {
42 | text: string
43 | matches: boolean
44 | }[]
45 | properties: CSSProperty[]
46 | filename?: string
47 | startLine?: number
48 | startColumn?: number
49 | propertyText?: string
50 | weightHighBits?: number // the priority value of the layer level (0 by default)
51 | weightLowBits?: number // the priority value of the selector level (calculated from selector by default)
52 | inactive?: boolean
53 | styleScope?: string | number
54 | }
55 |
56 | export type GetMatchedRulesResponses = {
57 | inline: CSSProperty[]
58 | inlineText?: string
59 | rules: CSSRule[]
60 | }
61 |
62 | export type GetInheritedRulesResponses = {
63 | rules: CSSRule[][]
64 | }
65 |
66 | export type GetAllComputedStylesResponses = {
67 | properties: { name: string; value: string }[]
68 | }
69 |
70 | export interface Observer {
71 | disconnect(): void
72 | }
73 |
74 | export type IntersectionStatus = {
75 | intersectionRatio: number
76 | boundingClientRect: BoundingClientRect
77 | intersectionRect: BoundingClientRect
78 | relativeRect: BoundingClientRect
79 | time: number
80 | }
81 |
82 | export const enum ResizeObserverMode {
83 | ContentBox = 1,
84 | BorderBox = 2,
85 | }
86 |
87 | export type ResizeStatus = {
88 | boundingContentBoxWidth: number
89 | boundingContentBoxHeight: number
90 | boundingBorderBoxWidth: number
91 | boundingBorderBoxHeight: number
92 | }
93 |
94 | export type MediaQueryStatus = {
95 | minWidth?: number
96 | maxWidth?: number
97 | width?: number
98 | minHeight?: number
99 | maxHeight?: number
100 | height?: number
101 | orientation?: string
102 | }
103 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/src/env.ts:
--------------------------------------------------------------------------------
1 | import * as glassEasel from 'glass-easel'
2 | import { AssociatedBackend } from './backend'
3 | import { CodeSpace } from './space'
4 |
5 | /**
6 | * A mini-program API environment
7 | *
8 | * Each environment manages multiple backend contexts.
9 | * However, a backend context should be exclusively managed by a single environment
10 | * (to avoid `StyleScopeId` confliction).
11 | * It is able to create multiple `CodeSpace` within the environment.
12 | */
13 | export class MiniProgramEnv {
14 | private codeSpaceMap: { [id: string]: CodeSpace }
15 | private globalCodeSpace: CodeSpace
16 |
17 | /**
18 | * Create an empty mini-program API environment
19 | */
20 | constructor() {
21 | this.codeSpaceMap = Object.create(null) as { [id: string]: CodeSpace }
22 | this.globalCodeSpace = new CodeSpace(this, false, {})
23 | }
24 |
25 | /**
26 | * Get a global code space in which global components can be defined
27 | *
28 | * External glass-easel based components can be defined in this code space.
29 | * All global components should be defined before any other `CodeSpace` created!
30 | */
31 | getGlobalCodeSpace(): CodeSpace {
32 | return this.globalCodeSpace
33 | }
34 |
35 | /**
36 | * Associate a backend context
37 | *
38 | * If `backendContext` is not given, the DOM-based backend is used
39 | * (causes failure if running outside browsers).
40 | * The backend context SHOULD NOT be associated by other environments!
41 | */
42 | associateBackend(backendContent?: glassEasel.GeneralBackendContext): AssociatedBackend {
43 | const ctx = backendContent ?? new glassEasel.CurrentWindowBackendContext()
44 | return new AssociatedBackend(this, ctx)
45 | }
46 |
47 | /**
48 | * Create a component space that can manage WXML, WXSS, JS and static JSON config files
49 | *
50 | * The space can be specified as a *main* space.
51 | * This makes some features available:
52 | * * the `app.wxss` will be used as a style sheet for all root components in the space;
53 | * * the `StyleIsolation.Shared` is accepted for components in the space.
54 | * Non-main spaces usually act as plugins.
55 | * `publicComponents` is a map for specifying a map of aliases and component paths.
56 | */
57 | createCodeSpace(
58 | id: string,
59 | isMainSpace: boolean,
60 | publicComponents = Object.create(null) as { [alias: string]: string },
61 | ): CodeSpace {
62 | const cs = new CodeSpace(this, isMainSpace, publicComponents, this.globalCodeSpace)
63 | this.codeSpaceMap[id] = cs
64 | return cs
65 | }
66 |
67 | /**
68 | * Get a component space by the `id` specified when created
69 | */
70 | getCodeSpace(id: string): CodeSpace | undefined {
71 | return this.codeSpaceMap[id]
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/glass-easel/src/backend/domlike_backend_protocol.ts:
--------------------------------------------------------------------------------
1 | import { type Element as GlassEaselElement } from '../element'
2 | import { type EventBubbleStatus, type EventOptions, type MutLevel } from '../event'
3 | import { type BackendMode } from './shared'
4 | import type * as suggestedBackend from './suggested_backend_protocol'
5 |
6 | export interface Context extends Partial> {
7 | mode: BackendMode.Domlike
8 | destroy(): void
9 | getWindowWidth(): number
10 | getWindowHeight(): number
11 | getDevicePixelRatio(): number
12 | getTheme(): string
13 | registerStyleSheetContent(path: string, content: unknown): void
14 | appendStyleSheetPath(path: string, styleScope?: number): number
15 | disableStyleSheet(index: number): void
16 | render(cb: (err: Error | null) => void): void
17 | getRootNode(): Element
18 | document: {
19 | createElement(tagName: string): Element
20 | createTextNode(content: string): Element
21 | createDocumentFragment(): Element
22 | }
23 | associateValue(element: Element, value: GlassEaselElement): void
24 | onEvent(
25 | listener: (
26 | target: GlassEaselElement,
27 | type: string,
28 | detail: unknown,
29 | options: EventOptions,
30 | ) => EventBubbleStatus | void,
31 | ): void
32 | setListenerStats(element: Element, type: string, capture: boolean, mutLevel: MutLevel): void
33 | setModelBindingStat(
34 | element: Element,
35 | attributeName: string,
36 | listener: ((newValue: unknown) => void) | null,
37 | ): void
38 | }
39 |
40 | export interface Element extends Partial {
41 | _$wxArgs?: {
42 | modelListeners: { [name: string]: ((newValue: unknown) => void) | null }
43 | }
44 | __wxElement?: GlassEaselElement
45 | appendChild(child: Element): void
46 | removeChild(child: Element, index?: number): void
47 | insertBefore(child: Element, before?: Element, index?: number): void
48 | replaceChild(child: Element, oldChild?: Element, index?: number): void
49 | tagName: string
50 | id: string
51 | classList: {
52 | add(elementClass: string): void
53 | remove(elementClass: string): void
54 | }
55 | setAttribute(name: string, value: unknown): void
56 | removeAttribute(name: string): void
57 | textContent: string
58 | nextSibling: Element | undefined
59 | childNodes: Element[]
60 | parentNode: Element | null
61 | addEventListener(
62 | type: K,
63 | listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => unknown,
64 | options?: boolean | AddEventListenerOptions,
65 | ): void
66 | addEventListener(
67 | type: string,
68 | listener: EventListenerOrEventListenerObject,
69 | options?: boolean | AddEventListenerOptions,
70 | ): void
71 | }
72 |
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 |
3 | module.exports = {
4 | root: true,
5 | parserOptions: {
6 | ecmaVersion: 9,
7 | sourceType: 'module',
8 | },
9 | parser: '@typescript-eslint/parser',
10 | plugins: ['@typescript-eslint', 'import', 'promise', 'prettier'],
11 | overrides: [
12 | {
13 | files: ['*.ts'],
14 | extends: [
15 | 'plugin:@typescript-eslint/recommended',
16 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',
17 | 'eslint-config-prettier',
18 | ],
19 | parserOptions: {
20 | project: path.join(__dirname, 'tsconfig.json'),
21 | },
22 | rules: {
23 | '@typescript-eslint/no-explicit-any': 'off',
24 | '@typescript-eslint/explicit-module-boundary-types': 'off',
25 | '@typescript-eslint/no-non-null-assertion': 'off',
26 | '@typescript-eslint/no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
27 | '@typescript-eslint/space-before-function-paren': [
28 | 'error',
29 | { anonymous: 'always', named: 'never', asyncArrow: 'always' },
30 | ],
31 | '@typescript-eslint/no-redeclare': ['error'],
32 | '@typescript-eslint/no-unsafe-argument': 'off',
33 | '@typescript-eslint/no-this-alias': 'off',
34 | '@typescript-eslint/no-unsafe-enum-comparison': 'off',
35 | '@typescript-eslint/unbound-method': ['error', { ignoreStatic: true }],
36 | },
37 | },
38 | ],
39 | extends: [
40 | 'eslint:recommended',
41 | 'airbnb-base',
42 | 'plugin:promise/recommended',
43 | 'eslint-config-prettier',
44 | ],
45 | env: {
46 | es6: true,
47 | jest: true,
48 | },
49 | rules: {
50 | 'comma-dangle': ['error', 'always-multiline'],
51 | 'handle-callback-err': ['error', '^(err|error)$'],
52 | 'no-catch-shadow': 'error',
53 | 'no-underscore-dangle': 'off',
54 | 'object-curly-spacing': ['error', 'always'],
55 | 'max-classes-per-file': 'off',
56 | 'no-unused-vars': 'off',
57 | 'no-multi-assign': 'off',
58 | 'lines-between-class-members': 'off',
59 | 'import/prefer-default-export': 'off',
60 | 'import/no-unresolved': 'off',
61 | 'import/extensions': 'off',
62 | 'no-shadow': 'off',
63 | 'prefer-destructuring': 'off',
64 | 'no-continue': 'off',
65 | 'no-use-before-define': 'off',
66 | 'no-dupe-class-members': 'off',
67 | 'func-names': 'off',
68 | 'space-before-function-paren': 'off',
69 | 'no-lonely-if': 'off',
70 | 'no-param-reassign': ['error', { props: false }],
71 | 'no-redeclare': 'off',
72 | 'prettier/prettier': 'warn',
73 | '@typescript-eslint/consistent-type-imports': [
74 | 'warn',
75 | {
76 | prefer: 'type-imports',
77 | fixStyle: 'inline-type-imports',
78 | },
79 | ],
80 | yoda: ['warn', 'never', { onlyEquality: true }],
81 | },
82 | }
83 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/src/types.ts:
--------------------------------------------------------------------------------
1 | import { type DeepCopyKind, type typeUtils as utils } from 'glass-easel'
2 | import type { DefinitionFilter, GeneralBehavior } from './behavior'
3 | import { type GeneralComponent } from './component'
4 | import type { RelationParams } from './builder/type_utils'
5 |
6 | export { typeUtils as utils } from 'glass-easel'
7 |
8 | export const enum StyleIsolation {
9 | Isolated = 'isolated',
10 | ApplyShared = 'apply-shared',
11 | Shared = 'shared',
12 | PageIsolated = 'page-isolated',
13 | PageApplyShared = 'page-apply-shared',
14 | PageShared = 'page-shared',
15 | }
16 |
17 | export type ComponentStaticConfig = {
18 | component?: boolean
19 | usingComponents?: { [name: string]: string }
20 | componentGenerics?: {
21 | [name: string]: true | { default?: string }
22 | }
23 | componentPlaceholder?: { [name: string]: string }
24 | styleIsolation?: StyleIsolation
25 | /** @obsolete */
26 | addGlobalClass?: boolean
27 | pureDataPattern?: string
28 | }
29 |
30 | export type ComponentDefinitionOptions = {
31 | multipleSlots?: boolean
32 | dynamicSlots?: boolean
33 | pureDataPattern?: RegExp
34 | virtualHost?: boolean
35 | dataDeepCopy?: DeepCopyKind
36 | propertyPassingDeepCopy?: DeepCopyKind
37 | propertyEarlyInit?: boolean
38 | propertyComparer?: (a: any, b: any) => boolean
39 | }
40 |
41 | type ComponentMethod = utils.ComponentMethod
42 |
43 | export type BehaviorDefinition<
44 | TData extends utils.DataList,
45 | TProperty extends utils.PropertyList,
46 | TMethod extends utils.MethodList,
47 | TComponentExport,
48 | > = {
49 | behaviors?: GeneralBehavior[]
50 | properties?: TProperty
51 | data?: TData | (() => TData)
52 | observers?:
53 | | {
54 | fields?: string
55 | observer: ComponentMethod
56 | }[]
57 | | { [fields: string]: ComponentMethod }
58 | methods?: TMethod
59 | created?: ComponentMethod
60 | attached?: ComponentMethod
61 | ready?: ComponentMethod
62 | moved?: ComponentMethod
63 | detached?: ComponentMethod
64 | lifetimes?: { [name: string]: ComponentMethod }
65 | pageLifetimes?: { [name: string]: ComponentMethod }
66 | relations?: Record
67 | externalClasses?: string[]
68 | definitionFilter?: DefinitionFilter
69 | export?: (source: GeneralComponent | null) => TComponentExport
70 | }
71 |
72 | export type ComponentDefinition<
73 | TData extends utils.DataList,
74 | TProperty extends utils.PropertyList,
75 | TMethod extends utils.MethodList,
76 | TComponentExport,
77 | > = {
78 | options?: ComponentDefinitionOptions
79 | } & BehaviorDefinition
80 |
81 | export type GeneralComponentDefinition = ComponentDefinition
82 |
83 | export type PageDefinition<
84 | TData extends utils.DataList,
85 | TExtraFields extends { [k: PropertyKey]: any },
86 | > = TExtraFields & {
87 | options?: ComponentDefinitionOptions
88 | behaviors?: GeneralBehavior[]
89 | data?: TData
90 | }
91 |
--------------------------------------------------------------------------------
/glass-easel-stylesheet-compiler/src/step.rs:
--------------------------------------------------------------------------------
1 | use std::ops::{Deref, DerefMut};
2 |
3 | use cssparser::{BasicParseError, Token};
4 |
5 | use super::error;
6 |
7 | pub(crate) struct StepParser<'i, 't, 'a> {
8 | parser: &'a mut cssparser::Parser<'i, 't>,
9 | }
10 |
11 | impl<'i, 't, 'a> Deref for StepParser<'i, 't, 'a> {
12 | type Target = cssparser::Parser<'i, 't>;
13 |
14 | fn deref(&self) -> &Self::Target {
15 | self.parser
16 | }
17 | }
18 |
19 | impl<'i, 't, 'a> DerefMut for StepParser<'i, 't, 'a> {
20 | fn deref_mut(&mut self) -> &mut ::Target {
21 | self.parser
22 | }
23 | }
24 |
25 | impl<'i, 't, 'a> StepParser<'i, 't, 'a> {
26 | pub(crate) fn wrap(parser: &'a mut cssparser::Parser<'i, 't>) -> Self {
27 | Self { parser }
28 | }
29 |
30 | pub(crate) fn position(&self) -> error::Position {
31 | let loc = self.parser.current_source_location();
32 | error::Position {
33 | line: loc.line,
34 | utf16_col: loc.column - 1,
35 | }
36 | }
37 |
38 | pub(crate) fn peek(&mut self) -> Result, BasicParseError<'i>> {
39 | self.parser.skip_whitespace();
40 | self.peek_including_whitespace()
41 | }
42 |
43 | pub(crate) fn peek_including_whitespace(
44 | &mut self,
45 | ) -> Result, BasicParseError<'i>> {
46 | let position = self.position();
47 | let state = self.parser.state();
48 | let ret = self.parser.next_including_whitespace().map(|x| x.clone());
49 | self.parser.reset(&state);
50 | ret.map(|token| StepToken { token, position })
51 | }
52 |
53 | pub(crate) fn next(&mut self) -> Result, BasicParseError<'i>> {
54 | self.parser.skip_whitespace();
55 | self.next_including_whitespace()
56 | }
57 |
58 | pub(crate) fn next_including_whitespace(
59 | &mut self,
60 | ) -> Result, BasicParseError<'i>> {
61 | let position = self.position();
62 | let token = self.parser.next_including_whitespace().map(|x| x.clone())?;
63 | Ok(StepToken { token, position })
64 | }
65 |
66 | pub(crate) fn try_parse(&mut self, thing: F) -> Result
67 | where
68 | F: FnOnce(&mut StepParser<'i, 't, '_>) -> Result,
69 | {
70 | self.parser
71 | .try_parse(|parser| thing(&mut StepParser { parser }))
72 | }
73 | }
74 |
75 | #[derive(Debug, Clone)]
76 | pub(crate) struct StepToken<'i> {
77 | pub(crate) token: Token<'i>,
78 | pub(crate) position: error::Position,
79 | }
80 |
81 | impl<'i> Deref for StepToken<'i> {
82 | type Target = cssparser::Token<'i>;
83 |
84 | fn deref(&self) -> &Self::Target {
85 | &self.token
86 | }
87 | }
88 |
89 | impl<'i> StepToken<'i> {
90 | pub(crate) fn wrap(token: Token<'i>, position: error::Position) -> Self {
91 | Self { token, position }
92 | }
93 |
94 | pub(crate) fn wrap_at(token: Token<'i>, other: &Self) -> Self {
95 | let position = other.position.clone();
96 | Self { token, position }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/glass-easel-stylesheet-compiler/src/output.rs:
--------------------------------------------------------------------------------
1 | use std::fmt::Write;
2 |
3 | use cssparser::{ToCss, Token, TokenSerializationType};
4 | use sourcemap::{SourceMap, SourceMapBuilder};
5 |
6 | use crate::step::StepToken;
7 |
8 | pub struct StyleSheetOutput {
9 | s: String,
10 | prev_ser_type: TokenSerializationType,
11 | source_map: SourceMapBuilder,
12 | source_id: u32,
13 | utf16_len: u32,
14 | }
15 |
16 | impl StyleSheetOutput {
17 | pub(crate) fn new(path: &str, source_css: &str) -> Self {
18 | let mut source_map = SourceMapBuilder::new(None);
19 | let source_id = source_map.add_source(path);
20 | source_map.set_source_contents(source_id, Some(source_css));
21 | Self {
22 | s: String::new(),
23 | prev_ser_type: TokenSerializationType::Nothing,
24 | source_id,
25 | source_map,
26 | utf16_len: 0,
27 | }
28 | }
29 |
30 | pub fn write(&self, mut w: impl std::io::Write) -> std::io::Result<()> {
31 | w.write_all(self.s.as_bytes())
32 | }
33 |
34 | pub fn write_str(&self, mut w: impl std::fmt::Write) -> std::fmt::Result {
35 | write!(w, "{}", self.s)
36 | }
37 |
38 | pub fn write_source_map(self, w: impl std::io::Write) -> Result<(), sourcemap::Error> {
39 | self.source_map.into_sourcemap().to_writer(w)
40 | }
41 |
42 | pub fn extract_source_map(self) -> SourceMap {
43 | self.source_map.into_sourcemap()
44 | }
45 |
46 | pub(crate) fn cur_utf8_len(&self) -> usize {
47 | self.s.len()
48 | }
49 |
50 | pub(crate) fn get_output_segment(&self, range: std::ops::Range) -> &str {
51 | &self.s[range]
52 | }
53 |
54 | pub(crate) fn append_raw(&mut self, s: &str) {
55 | self.prev_ser_type = TokenSerializationType::Nothing;
56 | let output_start_pos = self.s.len();
57 | self.s += s;
58 | self.utf16_len += str::encode_utf16(&self.s[output_start_pos..]).count() as u32;
59 | }
60 |
61 | pub(crate) fn append_token(&mut self, token: StepToken, src: Option) {
62 | let next_ser_type = token.serialization_type();
63 | if self
64 | .prev_ser_type
65 | .needs_separator_when_before(next_ser_type)
66 | {
67 | write!(&mut self.s, " ").unwrap();
68 | self.utf16_len += 1;
69 | }
70 | self.prev_ser_type = next_ser_type;
71 | let output_start_pos = self.s.len();
72 | token.to_css(&mut self.s).unwrap();
73 | let name = src.map(|x| {
74 | let s = x.to_css_string();
75 | self.source_map.add_name(&s)
76 | });
77 | self.source_map.add_raw(
78 | 0,
79 | self.utf16_len,
80 | token.position.line,
81 | token.position.utf16_col,
82 | Some(self.source_id),
83 | name,
84 | );
85 | self.utf16_len += str::encode_utf16(&self.s[output_start_pos..]).count() as u32;
86 | }
87 |
88 | pub(crate) fn append_token_space_preserved(&mut self, token: StepToken, src: Option) {
89 | if let Token::WhiteSpace(_) = &*token {
90 | self.prev_ser_type = token.serialization_type();
91 | self.s.push(' ');
92 | self.utf16_len += 1;
93 | } else {
94 | self.append_token(token, src);
95 | }
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/interaction/trait_behavior.md:
--------------------------------------------------------------------------------
1 | # Trait Behaviors
2 |
3 | ## 定义和实现 trait behavior
4 |
5 | trait behaviors 是一种组件间共享接口的机制,也可以用于给组件归类(类似于一些编程语言的 trait )。
6 |
7 | 在 TypeScript 下,通过 trait behaviors ,可以定义一组接口方法,若干组件都可以实现这组接口方法。这样,在外部访问这些组件时,可以不关注组件具体是什么,只需要关心组件提供的接口。例如,在 TypeScript 中定义并实现一个 trait behavior :
8 |
9 | ```ts
10 | // 定义一个 trait behavior
11 | interface AddMinus {
12 | add(a: number, b: number): number
13 | minus(a: number, b: number): number
14 | }
15 | const addMinusTrait = componentSpace.defineTraitBehavior()
16 |
17 | // 使用 Chaining API 实现 trait behavior
18 | const childComponent = componentSpace.define()
19 | .implement(addMinusTrait, {
20 | add(a, b) {
21 | return a + b
22 | },
23 | minus(a, b) {
24 | return a - b
25 | },
26 | })
27 | .defineComponent()
28 | // 或在 init 中实现 trait behavior
29 | const childComponent = componentSpace.define()
30 | .init(function ({ implement }) {
31 | implement(addMinusTrait, {
32 | add(a, b) {
33 | return a + b
34 | },
35 | minus(a, b) {
36 | return a - b
37 | },
38 | })
39 | })
40 | .defineComponent()
41 | ```
42 |
43 | 在从组件外访问这个组件时,可以使用 trait behavior :
44 |
45 | ```ts
46 | const myComponent = componentSpace.define()
47 | .usingComponents({
48 | child: childComponent,
49 | })
50 | .template(compileTemplate(`
51 |
52 | `))
53 | .lifetime('attached', function () {
54 | // 获取到子组件对应的节点
55 | const child = this.shadowRoot.querySelector('#c')!
56 | // 获得它对应的 addMinusTrait 的实现
57 | const impl = child.traitBehavior(addMinusTrait)!
58 | // 调用其中方法
59 | impl.add(1, 2) === 3 // true
60 | impl.minus(5, 3) === 2 // true
61 | })
62 | ```
63 |
64 | 这样,在使用组件时就不需要关心组件具体实现,只需要关心组件提供的 trait behavior ,利于代码解耦。
65 |
66 | ## 含接口转换的 trait behavior
67 |
68 | trait behavior 本身可以做一些接口转换,使得提供的接口比需要实现的接口更丰富。
69 |
70 | 例如, `addMinusTrait` 提供 `add` 和 `minus` 两个接口方法,但可以只要求实现者实现 `add` 接口方法:
71 |
72 | ```ts
73 | interface Add {
74 | add(a: number, b: number): number
75 | }
76 | interface AddMinus {
77 | add(a: number, b: number): number
78 | minus(a: number, b: number): number
79 | }
80 | // 这个 trait behavior 要求实现 add 接口方法,但可以提供 add 和 minus 两个接口方法
81 | const addMinusTrait = componentSpace.defineTraitBehavior((impl) => ({
82 | // 具体而言:
83 | // 入参中的 impl 是对 interface Add 的实现
84 | // 这里需要返回对 interface AddMinus 的实现
85 | add(a: number, b: number): number {
86 | return impl.add(a, b)
87 | },
88 | // 需要额外实现 minus 接口方法
89 | minus(a: number, b: number): number {
90 | return impl.add(a, -b)
91 | },
92 | }))
93 |
94 | const childComponent = componentSpace.define()
95 | .implement(addMinusTrait, {
96 | // 对于实现者,只需要实现 add 接口方法
97 | add(a, b) {
98 | return a + b
99 | }
100 | })
101 | .defineComponent()
102 |
103 | const myComponent = componentSpace.define()
104 | .usingComponents({
105 | child: childComponent,
106 | })
107 | .template(compileTemplate(`
108 |
109 | `))
110 | .lifetime('attached', function () {
111 | const child = this.shadowRoot.querySelector('#c')!
112 | const impl = child.traitBehavior(addMinusTrait)!
113 | // 可以调用 add 和 minus 两个接口
114 | impl.add(1, 2) === 3 // true
115 | impl.minus(5, 3) === 2 // true
116 | })
117 | ```
118 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-typescript/src/cli.ts:
--------------------------------------------------------------------------------
1 | import { parseArgs } from 'util'
2 | import * as ts from 'typescript'
3 | import { TmplGroup } from 'glass-easel-template-compiler'
4 | import { type Diagnostic, DiagnosticLevel, Server } from './server'
5 |
6 | const { values } = parseArgs({
7 | args: process.argv.slice(2),
8 | options: {
9 | help: {
10 | type: 'boolean',
11 | },
12 | path: {
13 | type: 'string',
14 | short: 'p',
15 | default: '.',
16 | },
17 | verbose: {
18 | type: 'boolean',
19 | short: 'v',
20 | },
21 | strict: {
22 | type: 'boolean',
23 | short: 's',
24 | },
25 | watch: {
26 | type: 'boolean',
27 | short: 'w',
28 | },
29 | templateBackendConfig: {
30 | type: 'string',
31 | short: 'c',
32 | },
33 | },
34 | strict: true,
35 | })
36 |
37 | if (values.help) {
38 | // eslint-disable-next-line no-console
39 | console.log(`Usage: miniprogram-typescript-check [options]
40 |
41 | Options:
42 | -h, --help Show this help message and exit
43 | -p, --path The path of the mini-program project (default: .)
44 | -c, --templateBackendConfig The path of the template backend config (including the extension)
45 | -w, --watch Watch the project and analyze on change
46 | -s, --strict Enable strict mode (avoid using \`any\` types when possible)
47 | -v, --verbose Print verbose messages
48 | `)
49 | process.exit(0)
50 | }
51 |
52 | const logDiagnostic = (diag: Diagnostic) => {
53 | // eslint-disable-next-line no-console
54 | if (diag.level === DiagnosticLevel.Error) {
55 | // eslint-disable-next-line no-console
56 | console.error(server.formatDiagnostic(diag))
57 | } else if (diag.level === DiagnosticLevel.Warning) {
58 | // eslint-disable-next-line no-console
59 | console.warn(server.formatDiagnostic(diag))
60 | } else if (diag.level === DiagnosticLevel.Info) {
61 | // eslint-disable-next-line no-console
62 | console.info(server.formatDiagnostic(diag))
63 | } else {
64 | // eslint-disable-next-line no-console
65 | console.log(server.formatDiagnostic(diag))
66 | }
67 | }
68 |
69 | let success = true
70 |
71 | const server = new Server({
72 | typescriptNodeModule: ts,
73 | tmplGroup: new TmplGroup(),
74 | projectPath: values.path,
75 | templateBackendConfigPath: values.templateBackendConfig,
76 | verboseMessages: values.verbose,
77 | strictMode: values.strict,
78 | onFirstScanDone() {
79 | this.getConfigErrors().forEach((diag) => {
80 | success = false
81 | logDiagnostic(diag)
82 | })
83 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
84 | ;(async () => {
85 | await this.waitPendingAsyncTasks()
86 | if (!values.watch) {
87 | server.end()
88 | if (!success) process.exit(1)
89 | }
90 | return undefined
91 | })()
92 | },
93 | onDiagnosticsNeedUpdate(fullPath: string) {
94 | // eslint-disable-next-line @typescript-eslint/no-floating-promises
95 | ;(async () => {
96 | const diags = await this.analyzeWxmlFile(fullPath)
97 | if (diags.length) success = false
98 | diags.forEach(logDiagnostic)
99 | return undefined
100 | })()
101 | },
102 | })
103 |
--------------------------------------------------------------------------------
/glass-easel/src/backend/backend_protocol.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 |
3 | import { type Element as GlassEaselElement } from '../element'
4 | import { type Event, type EventBubbleStatus, type EventOptions, type MutLevel } from '../event'
5 | import { type SlotMode } from '../shadow_root'
6 | import { type BackendMode } from './shared'
7 | import type * as suggestedBackend from './suggested_backend_protocol'
8 |
9 | export * from './shared'
10 |
11 | export interface Context extends Partial> {
12 | mode: BackendMode.Shadow
13 | destroy(): void
14 | getWindowWidth(): number
15 | getWindowHeight(): number
16 | getDevicePixelRatio(): number
17 | getTheme(): string
18 | registerStyleSheetContent(path: string, content: unknown): void
19 | appendStyleSheetPath(path: string, styleScope?: number): number
20 | disableStyleSheet(index: number): void
21 | render(cb: (err: Error | null) => void): void
22 | getRootNode(): ShadowRootContext
23 | createFragment(): Element
24 | onEvent(
25 | createEvent: (type: string, detail: unknown, options: EventOptions) => Event,
26 | listener: (
27 | event: Event,
28 | currentTarget: GlassEaselElement,
29 | mark: Record | null,
30 | target: GlassEaselElement,
31 | isCapture: boolean,
32 | ) => EventBubbleStatus | void,
33 | ): void
34 | }
35 |
36 | export interface Element extends Partial> {
37 | __wxElement?: GlassEaselElement
38 | release(): void
39 | associateValue(v: GlassEaselElement): void
40 | getShadowRoot(): ShadowRootContext | undefined
41 | appendChild(child: Element): void
42 | removeChild(child: Element, index?: number): void
43 | insertBefore(child: Element, before: Element, index?: number): void
44 | replaceChild(child: Element, oldChild: Element, index?: number): void
45 | spliceBefore(before: Element, deleteCount: number, list: Element): void
46 | spliceAppend(list: Element): void
47 | spliceRemove(before: Element, deleteCount: number): void
48 | setId(id: string): void
49 | setSlot(name: string): void
50 | setSlotName(slot: string): void
51 | setSlotElement(slot: Element | null): void
52 | setExternalSlot(slot: Element): void
53 | setInheritSlots(): void
54 | setStyle(styleText: string, styleSegmentIndex: number): void
55 | addClass(className: string): void
56 | removeClass(className: string): void
57 | clearClasses(): void
58 | setClassAlias(className: string, targets: string[]): void
59 | setAttribute(name: string, value: unknown): void
60 | removeAttribute(name: string): void
61 | setDataset(name: string, value: unknown): void
62 | setText(content: string): void
63 | setModelBindingStat(attributeName: string, listener: ((newValue: unknown) => void) | null): void
64 | setListenerStats(type: string, capture: boolean, mutLevel: MutLevel): void
65 | }
66 |
67 | export interface ShadowRootContext extends Element {
68 | createElement(logicalName: string, stylingName: string): Element
69 | createTextNode(content: string): Element
70 | createComponent(
71 | tagName: string,
72 | external: boolean,
73 | virtualHost: boolean,
74 | styleScope: number,
75 | extraStyleScope: number | null,
76 | externalClasses: string[] | undefined,
77 | slotMode: SlotMode | null,
78 | writeIdToDOM: boolean,
79 | ): Element
80 | createVirtualNode(virtualName: string): Element
81 | }
82 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/advanced/component_space.md:
--------------------------------------------------------------------------------
1 | # 组件空间
2 |
3 | ## 定义组件空间
4 |
5 | 组件空间是一组组件的集合。定义组件时,需要将它定义在某个组件空间里。
6 |
7 | ```js
8 | // 创建一个组件空间
9 | const componentSpace = new glassEasel.ComponentSpace()
10 |
11 | // 在这个组件空间中添加一个 hello-world 组件
12 | export const helloWorld = componentSpace.defineComponent({
13 | is: 'hello-world',
14 | })
15 | ```
16 |
17 | 在使用组件名字来指定组件时,只会在当前组件空间中查找组件,例如:
18 |
19 | ```js
20 | componentSpace.defineComponent({
21 | using: {
22 | // 在同一个组件空间下查找名为 hello-world 的组件
23 | hello: 'hello-world',
24 | },
25 | })
26 | ```
27 |
28 | ## 默认组件选项
29 |
30 | 组件空间可以为其中的组件指定一部分组件选项,这样,一些组件选项就不需要在每个组件定义时单独设置,例如:
31 |
32 | ```js
33 | componentSpace.updateComponentOptions({
34 | multipleSlots: true,
35 | })
36 | ```
37 |
38 | 这个组件空间中接下来定义的组件将统一具有这些选项。
39 |
40 | ## 默认组件
41 |
42 | 在组件空间中可以定义一个默认组件,当组件定义未找到时,会使用默认组件来代替。
43 |
44 | 通常,默认组件的名字是空字符串 `''` ,定义一个该名字的组件就可以将它作为默认组件:
45 |
46 | ```js
47 | export const helloWorld = componentSpace.defineComponent({
48 | is: '',
49 | })
50 | ```
51 |
52 | ## 全局引用
53 |
54 | 在实践中,可能有一些很常用的组件,在其他每个组件中都 using 引用会很繁琐。此时可以使用组件空间全局引用。例如:
55 |
56 | ```js
57 | const hotComponent = componentSpace.defineComponent({})
58 |
59 | componentSpace.setGlobalUsingComponent('hot', hotComponent)
60 | ```
61 |
62 | 这样相当于在所有组件中都引用了这个这个组件。
63 |
64 | 此外,可以将非组件节点也作为全局引用的目标,此时相当于给非组件节点赋予另一个名字。例如:
65 |
66 | ```js
67 | componentSpace.setGlobalUsingComponent('hot', 'div')
68 | ```
69 |
70 | 在组件的 `usingComponents` 中也可以重新 using 全局引用、赋予另一个名字。例如:
71 |
72 | ```js
73 | const myComponent = componentSpace.defineComponent({
74 | using: {
75 | 'another-hot': 'hot',
76 | },
77 | })
78 | ```
79 |
80 | ## 基组件空间
81 |
82 | 组件空间在创建时,可以导入另一个组件空间中的 **公开组件** 。例如:
83 |
84 | ```js
85 | // 创建一个基组件空间
86 | const baseComponentSpace = new glassEasel.ComponentSpace()
87 |
88 | // 在基组件空间中定义一个组件
89 | baseComponentSpace.defineComponent({
90 | is: 'base-component',
91 | })
92 |
93 | // 导出这个组件为公开组件
94 | baseComponentSpace.exportComponent('base-component', 'base-component')
95 |
96 | // 创建另一个组件空间,指定基组件空间
97 | const componentSpace = new glassEasel.ComponentSpace('', baseComponentSpace)
98 |
99 | // 可以使用基组件空间中导出的组件
100 | componentSpace.defineComponent({
101 | using: {
102 | base: 'base-component',
103 | },
104 | })
105 | ```
106 |
107 | ## 导入组件空间
108 |
109 | 组件空间中的公开组件可以被其他组件空间导入,例如:
110 |
111 | ```js
112 | // 创建一个组件空间
113 | const componentSpaceA = new glassEasel.ComponentSpace()
114 |
115 | // 在基组件空间中定义一个组件
116 | componentSpaceA.defineComponent({
117 | is: 'a-component',
118 | })
119 |
120 | // 导出这个组件,并为它命一个公开的名字
121 | componentSpaceA.exportComponent('public-name', 'a-component')
122 |
123 | // 创建另一个组件空间
124 | const componentSpaceB = new glassEasel.ComponentSpace()
125 |
126 | // 导入组件空间并指定它的引用 URL 前缀
127 | componentSpaceB.importSpace('space://space-a', componentSpaceA, false)
128 |
129 | // 可以使用导入的组件空间中定义的组件
130 | componentSpaceB.defineComponent({
131 | using: {
132 | base: 'space://space-a/public-name',
133 | },
134 | })
135 | ```
136 |
137 | 在导入组件空间时,也可以导入它的所有组件(不只是公开组件),例如:
138 |
139 | ```js
140 | // 创建另一个组件空间
141 | const componentSpaceC = new glassEasel.ComponentSpace()
142 |
143 | // 导入组件空间中的全部组件
144 | componentSpaceC.importSpace('space-private://space-a', componentSpaceA, true)
145 |
146 | // 可以使用导入的组件空间中定义的组件
147 | componentSpaceC.defineComponent({
148 | using: {
149 | base: 'space-private://space-a/a-component',
150 | },
151 | })
152 | ```
153 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/interaction/slot.md:
--------------------------------------------------------------------------------
1 | # slot 及 slot 类型
2 |
3 | ## 单一 slot
4 |
5 | 子组件可以通过 `` 节点来承载它在父组件上的子树内容,例如:
6 |
7 | ```js
8 | export const childComponent = componentSpace.defineComponent({
9 | template: compileTemplate(`
10 |
11 |
12 |
13 | `),
14 | })
15 | export const myComponent = componentSpace.defineComponent({
16 | using: {
17 | child: childComponent,
18 | },
19 | template: compileTemplate(`
20 |
25 | `),
26 | })
27 | ```
28 |
29 | 上例中 `` 将被放置到子组件中的 `` 位置上。
30 |
31 | ## 多 slot
32 |
33 | 有时子组件需要放置多个 slot 节点。此时可以指定组件的 `multipleSlots` 选项,并使用 `name` 来区分多个节点,例如:
34 |
35 | ```js
36 | export const childComponent = componentSpace.defineComponent({
37 | options: {
38 | multipleSlots: true,
39 | },
40 | template: compileTemplate(`
41 |
42 |
43 |
44 |
45 | `),
46 | })
47 | export const myComponent = componentSpace.defineComponent({
48 | using: {
49 | child: childComponent,
50 | },
51 | template: compileTemplate(`
52 |
53 |
54 | 这段内容被插入到 name="body" 的 slot 中
55 | 这段内容被插入到 name="footer" 的 slot 中
56 |
57 |
58 | `),
59 | })
60 | ```
61 |
62 | glass-easel 本身为单一 slot 应用了更多优化。激活 `multipleSlots` 同时也禁用了一些优化,对于不需要多 slot 的组件,最好不要激活这个选项。
63 |
64 | ## 动态 slot
65 |
66 | 上述两种 slot 类型要求(相同 name 的) slot 节点只有一个,重复的 `` 中只有第一个会生效。
67 |
68 | 但有时,需要在列表中放置 `` ,表示将 slot 内容重复多次。此时需要指定组件的 `dynamicSlots` 选项,例如:
69 |
70 | ```js
71 | export const childComponent = componentSpace.defineComponent({
72 | options: {
73 | dynamicSlots: true,
74 | },
75 | template: compileTemplate(`
76 |
77 |
78 |
79 | `),
80 | data: {
81 | list: ['A', 'B', 'C'],
82 | },
83 | })
84 | export const myComponent = componentSpace.defineComponent({
85 | using: {
86 | child: childComponent,
87 | },
88 | template: compileTemplate(`
89 |
94 | `),
95 | })
96 | ```
97 |
98 | 当 `dynamicSlots` 选项激活时,还可以向 slot 中传递数据,组件的使用者可以通过 `slot:` 来接收 slot 传递的数据,例如:
99 |
100 | ```js
101 | export const childComponent = componentSpace.defineComponent({
102 | options: {
103 | dynamicSlots: true,
104 | },
105 | template: compileTemplate(`
106 |
107 |
108 |
109 | `),
110 | data: {
111 | list: ['A', 'B', 'C'],
112 | },
113 | })
114 | export const myComponent = componentSpace.defineComponent({
115 | using: {
116 | child: childComponent,
117 | },
118 | template: compileTemplate(`
119 |
120 |
121 | {{ item }}
122 |
123 |
124 | `),
125 | })
126 | ```
127 |
128 | 使用 `slot:` 来接收数据时,也可以用 `=` 来指定一个别名。此外,接收数据的节点也可以是 `` 。例如:
129 |
130 | ```xml
131 |
132 | {{ index }}
133 | {{ item }}
134 |
135 | ```
136 |
137 | 通过 slot 传递的数据受到 [数据字段拷贝控制](../data_management/data_deep_copy.md) 选项中的 `propertyPassingDeepCopy` 选项控制。
138 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/cbindgen.toml:
--------------------------------------------------------------------------------
1 | # This is a template cbindgen.toml file with all of the default values.
2 | # Some values are commented out because their absence is the real default.
3 | #
4 | # See https://github.com/eqrion/cbindgen/blob/master/docs.md#cbindgentoml
5 | # for detailed documentation of every option here.
6 |
7 |
8 |
9 | language = "C++"
10 |
11 |
12 |
13 | ############## Options for Wrapping the Contents of the Header #################
14 |
15 | # header = "/* Text to put at the beginning of the generated file. Probably a license. */"
16 | # trailer = "/* Text to put at the end of the generated file */"
17 | # include_guard = "my_bindings_h"
18 | # pragma_once = true
19 | # autogen_warning = "/* Warning, this file is autogenerated by cbindgen. Don't modify this manually. */"
20 | include_version = false
21 | # namespace = "my_namespace"
22 | namespaces = ["glass_easel_template_compiler"]
23 | using_namespaces = []
24 | sys_includes = []
25 | includes = []
26 | no_includes = false
27 | after_includes = ""
28 |
29 |
30 |
31 |
32 | ############################ Code Style Options ################################
33 |
34 | braces = "SameLine"
35 | line_length = 100
36 | tab_width = 2
37 | documentation = true
38 | documentation_style = "auto"
39 | line_endings = "LF" # also "CR", "CRLF", "Native"
40 |
41 |
42 |
43 |
44 | ############################# Codegen Options ##################################
45 |
46 | style = "both"
47 | sort_by = "Name" # default for `fn.sort_by` and `const.sort_by`
48 | usize_is_size_t = true
49 |
50 |
51 |
52 | [defines]
53 | # "target_os = freebsd" = "DEFINE_FREEBSD"
54 | # "feature = serde" = "DEFINE_SERDE"
55 |
56 |
57 |
58 | [export]
59 | include = []
60 | exclude = []
61 | # prefix = "CAPI_"
62 | item_types = []
63 | renaming_overrides_prefixing = false
64 |
65 |
66 |
67 | [export.rename]
68 |
69 | [export.pre_body]
70 |
71 | [export.body]
72 |
73 |
74 | [export.mangle]
75 |
76 |
77 | [fn]
78 | rename_args = "None"
79 | # must_use = "MUST_USE_FUNC"
80 | # no_return = "NO_RETURN"
81 | # prefix = "START_FUNC"
82 | # postfix = "END_FUNC"
83 | args = "auto"
84 | sort_by = "Name"
85 |
86 |
87 |
88 |
89 | [struct]
90 | rename_fields = "None"
91 | # must_use = "MUST_USE_STRUCT"
92 | derive_constructor = false
93 | derive_eq = false
94 | derive_neq = false
95 | derive_lt = false
96 | derive_lte = false
97 | derive_gt = false
98 | derive_gte = false
99 |
100 |
101 |
102 |
103 | [enum]
104 | rename_variants = "None"
105 | # must_use = "MUST_USE_ENUM"
106 | add_sentinel = false
107 | prefix_with_name = false
108 | derive_helper_methods = false
109 | derive_const_casts = false
110 | derive_mut_casts = false
111 | # cast_assert_name = "ASSERT"
112 | derive_tagged_enum_destructor = false
113 | derive_tagged_enum_copy_constructor = false
114 | enum_class = true
115 | private_default_tagged_enum_constructor = false
116 |
117 |
118 |
119 |
120 | [const]
121 | allow_static_const = true
122 | allow_constexpr = false
123 | sort_by = "Name"
124 |
125 |
126 |
127 |
128 | [macro_expansion]
129 | bitflags = false
130 |
131 |
132 |
133 |
134 |
135 |
136 | ############## Options for How Your Rust library Should Be Parsed ##############
137 |
138 | [parse]
139 | parse_deps = false
140 | # include = []
141 | exclude = []
142 | clean = false
143 | extra_bindings = []
144 |
145 |
146 |
147 | [parse.expand]
148 | crates = []
149 | all_features = false
150 | default_features = true
151 | features = []
152 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/interaction/template_import.md:
--------------------------------------------------------------------------------
1 | # 模板引用
2 |
3 | ## 模板片段
4 |
5 | 模板中可以使用 `` 来提取一些公共的模板片段。
6 |
7 | 其中, `` 可以用来定义模板片段,片段中可以使用数据绑定,例如:
8 |
9 | ```xml
10 |
11 | {{ a }} + {{ b }} = {{ c }}
12 |
13 | ```
14 |
15 | 而 `` 可以用来引用模板片段,引用时需要指定数据绑定,例如:
16 |
17 | ```xml
18 |
19 | ```
20 |
21 | 注意, `data="{{ }}"` 内部就是一个对象形式定义。
22 |
23 | ## 跨组件的模板片段
24 |
25 | 如果需要在多个组件中使用同一个模板片段,则需要调整模板编译方式。
26 |
27 | 在模板编译时,首先需要创建一个 `TmplGroup` 对象。这个对象可以同时管理多个路径的模板代码,例如:
28 |
29 | ```js
30 | import { TmplGroup } from 'glass-easel-template-compiler'
31 |
32 | const group = new TmplGroup()
33 |
34 | // 添加一个模板路径及其对应的模板内容
35 | group.addTmpl('path/to/a/component', `
36 | text in a template
37 | `)
38 |
39 | // 添加另一个模板路径及其对应的模板内容
40 | group.addTmpl('path/to/another/component', `
41 | text in another template
42 | `)
43 |
44 | // 生成模板编译结果
45 | const genObjectSrc = `return ${group.getTmplGenObjectGroups()}`
46 | const genObjectGroupList = (new Function(genObjectSrc))() as { [key: string]: any }
47 | group.free()
48 |
49 | // 从编译结果中提取指定路径的模板
50 | const templateForAComponent = {
51 | groupList: genObjectGroupList,
52 | content: genObjectGroupList['path/to/a/template'],
53 | }
54 | const templateForAnotherComponent = {
55 | groupList: genObjectGroupList,
56 | content: genObjectGroupList['path/to/another/template'],
57 | }
58 | ```
59 |
60 | 这样,可以使用一个独立的模板路径来存放公共的模板片段,例如:
61 |
62 | ```js
63 | group.addTmpl('path/to/shared', `
64 |
65 | {{ a }} + {{ b }} = {{ c }}
66 |
67 | `)
68 | ```
69 |
70 | 在其他路径的模板中,可以使用 `` 引入模板片段:
71 |
72 | ```js
73 | group.addTmpl('path/to/a/component', `
74 |
75 |
76 |
77 | `)
78 | ```
79 |
80 | ## 模板文件引入
81 |
82 | 在引用其他路径的模板时,也可以使用 `` 直接将整个模板文件内容嵌入到当前位置上,例如:
83 |
84 | ```js
85 | group.addTmpl('path/to/shared', `
86 | some text in shared template
87 | `)
88 |
89 | group.addTmpl('path/to/a/component', `
90 |
91 |
92 |
93 | `)
94 | ```
95 |
96 | ## 嵌入 JavaScript 代码
97 |
98 | 在编译多个模板的同时,可以嵌入若干段 JavaScript 代码,与模板一同生成编译结果。
99 |
100 | 嵌入的每段 JavaScript 代码都必须是一个合法的 JavaScript 文件内容,并需要为它指定一个路径。例如:
101 |
102 | ```js
103 | group.addScript('path/to/script', `
104 | // JavaScript 文件内容
105 | `)
106 | ```
107 |
108 | 代码中可以访问 `require` 和 `exports` 。类似于 Node.js , `require` 用于导入其他路径的代码, `exports` 用于导出。例如:
109 |
110 | ```js
111 | exports.hello = function () {
112 | return 'Hello!'
113 | }
114 | ```
115 |
116 | 在模板文件中,可以通过 `` 来引用指定路径对应的 JavaScript 代码,并将这个 JavaScript 函数的返回值作为 wxs module 值。例如:
117 |
118 | ```xml
119 |
120 | ```
121 |
122 | 这样,模板中的数据绑定表达式中就可以访问 `helloModule` 变量(它的值就是对应 JavaScript 函数的导出)。
123 |
124 | ```xml
125 |
126 | {{ helloModule.hello() }}
127 | ```
128 |
129 | 此外, `` 也可以不使用 `src` 引入,而是直接将 JavaScript 代码内联在 `` 内部。
130 |
131 | ## 模板全局 JavaScript 代码
132 |
133 | 除了按路径嵌入的 JavaScript 代码,还可以添加一段全局 JavaScript 代码。
134 |
135 | ```js
136 | group.addExtraRuntimeScript(`
137 | // JavaScript 代码
138 | // ...
139 | `)
140 | ```
141 |
142 | 这段代码无论如何都会执行,它可以定义一些全局量供所有其他 JavaScript 代码使用,但这些全局量无法被模板中的数据绑定表达式直接访问到。
143 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/data_management/data_deep_copy.md:
--------------------------------------------------------------------------------
1 | # 数据字段拷贝控制
2 |
3 | ## 数据深拷贝
4 |
5 | 默认情况下,数据字段应用到模板前,会经过一次深拷贝。这样,无意中对组件实例的 `this.data` 进行的修改,将不会应用到模板上。例如:
6 |
7 | ```js
8 | export const myComponent = componentSpace.defineComponent({
9 | template: compileTemplate(`
10 | {{ a }}
11 | `),
12 | data: {
13 | a: 1,
14 | },
15 | lifetimes: {
16 | attached() {
17 | // 这样进行的更新不会应用到模板上
18 | this.data.a = 2
19 | },
20 | },
21 | })
22 | ```
23 |
24 | 这样虽然可以提供一些对错误逻辑的保护,但需要进行一次深拷贝。默认情况下,这次深拷贝不支持含有递归字段的数据,例如:
25 |
26 | ```js
27 | export const myComponent = componentSpace.defineComponent({
28 | data: {
29 | a: {},
30 | },
31 | lifetimes: {
32 | attached() {
33 | const a = {}
34 | a.b = a
35 | // a 是一个有递归字段的对象
36 | // 不能用于 setData ,否则会出错!
37 | this.setData({ a })
38 | },
39 | },
40 | })
41 | ```
42 |
43 | 此时可以更改组件的 `dataDeepCopy` 选项,使其支持递归字段:
44 |
45 | ```js
46 | export const myComponent = componentSpace.defineComponent({
47 | options: {
48 | dataDeepCopy: glassEasel.DeepCopyKind.SimpleWithRecursion,
49 | },
50 | data: {
51 | a: {},
52 | },
53 | lifetimes: {
54 | attached() {
55 | const a = {}
56 | a.b = a
57 | this.setData({ a })
58 | },
59 | },
60 | })
61 | ```
62 |
63 | 或者也可以完全禁用拷贝(这需要保证不能直接修改 `this.data` ),这样可以完全避免额外开销、对象的原型链能够保留、性能最优:
64 |
65 | ```js
66 | export const myComponent = componentSpace.defineComponent({
67 | options: {
68 | dataDeepCopy: glassEasel.DeepCopyKind.None,
69 | },
70 | data: {
71 | a: {},
72 | },
73 | lifetimes: {
74 | attached() {
75 | const a = {}
76 | a.b = a
77 | this.setData({ a })
78 | },
79 | },
80 | })
81 | ```
82 |
83 | 如果需要改变所有组件的这个选项,考虑更改 [组件空间](../advanced/component_space.md) 中的默认组件选项。
84 |
85 | ## 属性传递深拷贝
86 |
87 | 默认情况下,传递组件属性时,也会经过一次深拷贝。这样可以使得两个组件之间不共享对象、对一个组件的数据变更不会影响另一个组件。
88 |
89 | 但这个行为也会使得含有递归字段的对象在传递时出错。可以通过更改子组件的 `propertyPassingDeepCopy` 选项,使其属性支持递归字段:
90 |
91 | ```js
92 | export const childComponent = componentSpace.defineComponent({
93 | options: {
94 | propertyPassingDeepCopy: glassEasel.DeepCopyKind.SimpleWithRecursion,
95 | },
96 | properties: {
97 | a: Object,
98 | },
99 | })
100 |
101 | export const myComponent = componentSpace.defineComponent({
102 | options: {
103 | dataDeepCopy: glassEasel.DeepCopyKind.SimpleWithRecursion,
104 | },
105 | using: {
106 | child: childComponent,
107 | },
108 | template: compileTemplate(`
109 |
110 | `),
111 | data: {
112 | a: {},
113 | },
114 | lifetimes: {
115 | attached() {
116 | const a = {}
117 | a.b = a
118 | this.setData({ a })
119 | },
120 | },
121 | })
122 | ```
123 |
124 | 或者也可以完全禁用拷贝(这需要子组件保证不修改传入的对象),这样可以完全避免额外开销、对象的原型链能够保留、性能最优:
125 |
126 | ```js
127 | export const childComponent = componentSpace.defineComponent({
128 | options: {
129 | propertyPassingDeepCopy: glassEasel.DeepCopyKind.None,
130 | },
131 | properties: {
132 | a: Object,
133 | },
134 | })
135 |
136 | export const myComponent = componentSpace.defineComponent({
137 | options: {
138 | dataDeepCopy: glassEasel.DeepCopyKind.None,
139 | },
140 | using: {
141 | child: childComponent,
142 | },
143 | template: compileTemplate(`
144 |
145 | `),
146 | data: {
147 | a: {},
148 | },
149 | lifetimes: {
150 | attached() {
151 | const a = {}
152 | a.b = a
153 | this.setData({ a })
154 | },
155 | },
156 | })
157 | ```
158 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/data_management/data_observer.md:
--------------------------------------------------------------------------------
1 | # 数据监听器
2 |
3 | ## 监听数据字段变化
4 |
5 | 通过数据监听器,可以在某些属性或数据字段被设置时触发一个响应函数。
6 |
7 | 这个响应函数会在属性或数据字段应用到模板前被调用,可以在总体上减少模板更新次数从而提升性能。
8 |
9 | 在这个响应函数中,可以使用 `updateData` 来更新其他的数据字段,这些数据字段会响应函数执行完毕后生效。注意,虽然也可以使用 `setData` ,但数据监听器中的 `setData` 并不会立刻应用到模板上,而是像 `updateData` 一样、等到数据监听器执行完毕后才应用到模板上。
10 |
11 | 例如,可以在 `a` 和 `b` 两个字段中任何一个被设置时,触发一个响应函数:
12 |
13 | ```js
14 | // 使用 Definition API 添加数据监听器
15 | export const addComponent = componentSpace.defineComponent({
16 | template: compileTemplate(`
17 | {{ a }} + {{ b }} = {{ sum }}
18 | `),
19 | data: {
20 | a: 1,
21 | b: 2,
22 | sum: 3,
23 | },
24 | // 添加一个数据监听器
25 | observers: {
26 | 'a, b': function () {
27 | // 数据监听器中,最好使用 updateData 而非 setData
28 | this.updateData({
29 | sum: this.data.a + this.data.b,
30 | })
31 | },
32 | },
33 | lifetimes: {
34 | attached() {
35 | // 执行这个 setData 时,数据监听器会被执行
36 | this.setData({
37 | a: 3,
38 | })
39 | },
40 | },
41 | })
42 | // 或使用 Chaining API 添加数据监听器
43 | export const addComponent = componentSpace.define()
44 | .data(() => ({
45 | a: 1,
46 | b: 2,
47 | sum: 3,
48 | }))
49 | // 在 Chaining API 中,应传入数组来监听多个数据字段
50 | .observer(['a', 'b'], function () {
51 | this.updateData({
52 | sum: this.data.a + this.data.b,
53 | })
54 | })
55 | .lifetime('attached', function () {
56 | this.setData({
57 | a: 3,
58 | })
59 | })
60 | .registerComponent()
61 | // 或在 init 函数中添加数据监听器
62 | export const addComponent = componentSpace.define()
63 | .data(() => ({
64 | a: 1,
65 | b: 2,
66 | sum: 3,
67 | }))
68 | .init(function ({ lifetime, observer }) {
69 | // 应传入数组来监听多个数据字段
70 | observer(['a', 'b'], () => {
71 | this.updateData({
72 | sum: this.data.a + this.data.b,
73 | })
74 | })
75 | lifetime('attached', function () {
76 | this.setData({
77 | a: 3,
78 | })
79 | })
80 | })
81 | .registerComponent()
82 | ```
83 |
84 | 注意:数据监听器会在其所监听字段值被设置时触发,即使其值没有发生变化,监听器也会触发。
85 |
86 | 不要在数据监听器内设置被监听的数据字段本身,否则会无限循环:
87 |
88 | ```js
89 | export const addComponent = componentSpace.defineComponent({
90 | data: {
91 | a: 1,
92 | },
93 | observers: {
94 | 'a': function () {
95 | // 会导致无限循环!
96 | this.updateData({
97 | a: 1,
98 | })
99 | },
100 | },
101 | })
102 | ```
103 |
104 | ## 用数据监听器监听子字段
105 |
106 | 数据监听器也可以监听子字段,例如:
107 |
108 | ```js
109 | export const addComponent = componentSpace.defineComponent({
110 | data: {
111 | obj: { a: 1 },
112 | arr: [1, 2, 3],
113 | },
114 | // 添加一个数据监听器
115 | observers: {
116 | 'obj.a, arr[2]': function () {
117 | // 当 obj.a 或 arr[2] 被设置时会执行
118 | },
119 | },
120 | lifetimes: {
121 | attached() {
122 | // 执行这组更新时,数据监听器会被执行
123 | this.groupUpdate(() => {
124 | this.replaceDataOnPath(['obj', 'a'], 3)
125 | })
126 | },
127 | },
128 | })
129 | ```
130 |
131 | 若想监听所有子字段,可以使用通配符 `**` ,例如:
132 |
133 | ```js
134 | export const addComponent = componentSpace.defineComponent({
135 | data: {
136 | obj: { a: 1 },
137 | },
138 | observers: {
139 | 'obj.**': function () {
140 | // 当 obj 的任何子字段被设置时都会执行
141 | },
142 | },
143 | })
144 | ```
145 |
146 | 特别地,这样可以监听所有数据字段:
147 |
148 | ```js
149 | export const addComponent = componentSpace.defineComponent({
150 | observers: {
151 | '**': function () {
152 | // 当任何字段被设置时都会执行
153 | },
154 | },
155 | })
156 | ```
157 |
--------------------------------------------------------------------------------
/glass-easel-template-compiler/src/binding_map.rs:
--------------------------------------------------------------------------------
1 | use std::{collections::HashMap, fmt::Write};
2 |
3 | use crate::{escape::gen_lit_str, proc_gen::JsFunctionScopeWriter, TmplError};
4 |
5 | #[derive(Debug, Clone)]
6 | pub(crate) struct BindingMapCollector {
7 | overall_disabled: bool,
8 | fields: HashMap,
9 | }
10 |
11 | #[derive(Debug, Clone)]
12 | pub(crate) enum BindingMapField {
13 | Mapped(usize),
14 | Disabled,
15 | }
16 |
17 | impl BindingMapCollector {
18 | pub(crate) fn new() -> Self {
19 | Self {
20 | overall_disabled: false,
21 | fields: HashMap::new(),
22 | }
23 | }
24 |
25 | pub(crate) fn disable_all(&mut self) {
26 | self.overall_disabled = true;
27 | }
28 |
29 | pub(crate) fn add_field(&mut self, field: &str) -> Option {
30 | let x = self
31 | .fields
32 | .entry(field.to_owned())
33 | .or_insert_with(|| BindingMapField::Mapped(0));
34 | if let BindingMapField::Mapped(x) = x {
35 | let ret = *x;
36 | *x += 1;
37 | return Some(ret);
38 | }
39 | None
40 | }
41 |
42 | pub(crate) fn disable_field(&mut self, field: &str) {
43 | self.fields
44 | .insert(field.to_owned(), BindingMapField::Disabled);
45 | }
46 |
47 | pub(crate) fn get_field(&self, field: &str) -> Option<()> {
48 | if self.overall_disabled {
49 | return None;
50 | }
51 | self.fields.get(field).and_then(|x| match x {
52 | BindingMapField::Mapped(_) => Some(()),
53 | BindingMapField::Disabled => None,
54 | })
55 | }
56 |
57 | pub(crate) fn list_fields(&self) -> impl Iterator- {
58 | let overall_disabled = self.overall_disabled;
59 | self.fields.iter().filter_map(move |(key, field)| {
60 | if overall_disabled {
61 | return None;
62 | }
63 | match field {
64 | BindingMapField::Mapped(x) => Some((key.as_str(), *x)),
65 | BindingMapField::Disabled => None,
66 | }
67 | })
68 | }
69 | }
70 |
71 | #[derive(Debug, Clone)]
72 | pub struct BindingMapKeys {
73 | keys: Vec<(String, usize)>,
74 | }
75 |
76 | impl BindingMapKeys {
77 | pub(crate) fn new() -> Self {
78 | Self { keys: vec![] }
79 | }
80 |
81 | pub(crate) fn add(&mut self, key: &str, index: usize) {
82 | self.keys.push((key.to_string(), index))
83 | }
84 |
85 | pub(crate) fn is_empty(&self, bmc: &BindingMapCollector) -> bool {
86 | for (key, _) in self.keys.iter() {
87 | if bmc.get_field(key).is_some() {
88 | return false;
89 | }
90 | }
91 | true
92 | }
93 |
94 | pub(crate) fn to_proc_gen_write_map(
95 | &self,
96 | w: &mut JsFunctionScopeWriter,
97 | bmc: &BindingMapCollector,
98 | write_expr: impl FnOnce(&mut JsFunctionScopeWriter) -> Result<(), TmplError>,
99 | ) -> Result<(), TmplError> {
100 | w.expr_stmt(|w| {
101 | for (key, index) in self.keys.iter() {
102 | if bmc.get_field(key).is_some() {
103 | write!(w, "A[{}][{}]=", gen_lit_str(key), index)?;
104 | continue;
105 | }
106 | }
107 | w.function_args("D,E,T", |w| write_expr(w))?;
108 | Ok(())
109 | })?;
110 | Ok(())
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/glass-easel-miniprogram-adapter/tests/data_update.test.ts:
--------------------------------------------------------------------------------
1 | import * as glassEasel from 'glass-easel'
2 | import { tmpl } from './base/env'
3 | import { MiniProgramEnv } from '../src'
4 |
5 | const domHtml = (elem: glassEasel.Element): string => {
6 | const domElem = elem.getBackendElement() as unknown as Element
7 | return domElem.innerHTML
8 | }
9 |
10 | describe('data update', () => {
11 | test('various data update', () => {
12 | const env = new MiniProgramEnv()
13 | const codeSpace = env.createCodeSpace('', true)
14 |
15 | codeSpace.addComponentStaticConfig('path/to/comp', {})
16 | codeSpace.addCompiledTemplate(
17 | 'path/to/comp',
18 | tmpl(`
19 |
{{item}}
20 | {{obj.sub}}
21 | `),
22 | )
23 | codeSpace.componentEnv('path/to/comp', ({ Component }) => {
24 | Component()
25 | .data(() => ({
26 | arr: [1, 2, 3],
27 | obj: {
28 | sub: false,
29 | },
30 | }))
31 | .lifetime('attached', function () {
32 | this.groupUpdates(() => {
33 | this.spliceArrayDataOnPath(['arr'], 1, 1, [4, 5])
34 | this.replaceDataOnPath(['obj', 'sub'], true)
35 | })
36 | // eslint-disable-next-line no-use-before-define
37 | expect(domHtml(root.getComponent())).toBe(
38 | '1
4
5
3
true',
39 | )
40 | this.groupSetData(() => {
41 | this.updateData({
42 | arr: [123],
43 | })
44 | })
45 | // eslint-disable-next-line no-use-before-define
46 | expect(domHtml(root.getComponent())).toBe(
47 | '1
4
5
3
true',
48 | )
49 | this.applyDataUpdates()
50 | // eslint-disable-next-line no-use-before-define
51 | expect(domHtml(root.getComponent())).toBe('123
true')
52 | })
53 | .register()
54 | })
55 |
56 | const ab = env.associateBackend()
57 | const root = ab.createRoot('body', codeSpace, 'path/to/comp')
58 | glassEasel.Element.pretendAttached(root.getComponent())
59 | expect(domHtml(root.getComponent())).toBe('123
true')
60 | })
61 |
62 | test('setData callback', () =>
63 | new Promise((resolve) => {
64 | const env = new MiniProgramEnv()
65 | const codeSpace = env.createCodeSpace('', true)
66 |
67 | codeSpace.addComponentStaticConfig('path/to/comp', {})
68 | codeSpace.addCompiledTemplate(
69 | 'path/to/comp',
70 | tmpl(`
71 | {{a}}
72 | `),
73 | )
74 | codeSpace.componentEnv('path/to/comp', ({ Component }) => {
75 | Component()
76 | .data(() => ({
77 | a: 123,
78 | }))
79 | .init(function ({ setData, lifetime }) {
80 | lifetime('attached', () => {
81 | setData({ a: 789 }, () => {
82 | this.setData({ a: 456 }, () => {
83 | // eslint-disable-next-line no-use-before-define
84 | expect(domHtml(root.getComponent())).toBe('456')
85 | resolve(undefined)
86 | })
87 | })
88 | })
89 | })
90 | .register()
91 | })
92 |
93 | const ab = env.associateBackend()
94 | const root = ab.createRoot('body', codeSpace, 'path/to/comp')
95 | glassEasel.Element.pretendAttached(root.getComponent())
96 | expect(domHtml(root.getComponent())).toBe('789')
97 | }))
98 | })
99 |
--------------------------------------------------------------------------------
/glass-easel-stylesheet-compiler/src/error.rs:
--------------------------------------------------------------------------------
1 | use std::ops::Range;
2 |
3 | use serde::{Deserialize, Serialize};
4 |
5 | /// A location in source code.
6 | #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7 | pub struct Position {
8 | pub line: u32,
9 | pub utf16_col: u32,
10 | }
11 |
12 | impl Position {
13 | /// Get the line-column offsets (in UTF-16) in the source code.
14 | pub fn line_col_utf16<'s>(&self) -> (usize, usize) {
15 | (self.line as usize, self.utf16_col as usize)
16 | }
17 | }
18 |
19 | /// Parsing error object.
20 | #[derive(Debug, Clone, PartialEq)]
21 | pub struct ParseError {
22 | pub path: String,
23 | pub kind: ParseErrorKind,
24 | pub location: Range,
25 | }
26 |
27 | impl std::fmt::Display for ParseError {
28 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
29 | write!(
30 | f,
31 | "style sheet parsing error at {}:{}:{}-{}:{}: {}",
32 | self.path,
33 | self.location.start.line + 1,
34 | self.location.start.utf16_col + 1,
35 | self.location.end.line + 1,
36 | self.location.end.utf16_col + 1,
37 | self.kind,
38 | )
39 | }
40 | }
41 |
42 | impl std::error::Error for ParseError {}
43 |
44 | impl ParseError {
45 | /// The level of the error.
46 | pub fn level(&self) -> ParseErrorLevel {
47 | self.kind.level()
48 | }
49 |
50 | /// An error code.
51 | pub fn code(&self) -> u32 {
52 | self.kind.clone() as u32
53 | }
54 |
55 | /// Whether the error prevent a success compilation.
56 | pub fn prevent_success(&self) -> bool {
57 | self.level() >= ParseErrorLevel::Error
58 | }
59 | }
60 |
61 | #[derive(Clone, PartialEq, Eq)]
62 | pub enum ParseErrorKind {
63 | UnexpectedCharacter = 0x10001,
64 | IllegalImportPosition,
65 | HostSelectorCombination,
66 | }
67 |
68 | impl ParseErrorKind {
69 | fn static_message(&self) -> &'static str {
70 | match self {
71 | Self::UnexpectedCharacter => "unexpected character",
72 | Self::IllegalImportPosition => "`@import` should be placed at the start of the stylesheet (according to CSS standard)",
73 | Self::HostSelectorCombination => "`:host` selector combined with other selectors are not supported",
74 | }
75 | }
76 |
77 | pub fn level(&self) -> ParseErrorLevel {
78 | match self {
79 | Self::UnexpectedCharacter => ParseErrorLevel::Fatal,
80 | Self::IllegalImportPosition => ParseErrorLevel::Note,
81 | Self::HostSelectorCombination => ParseErrorLevel::Warn,
82 | }
83 | }
84 | }
85 |
86 | impl std::fmt::Debug for ParseErrorKind {
87 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
88 | write!(f, "{:?}", self.static_message())
89 | }
90 | }
91 |
92 | impl std::fmt::Display for ParseErrorKind {
93 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
94 | write!(f, "{}", self.static_message())
95 | }
96 | }
97 |
98 | #[repr(u8)]
99 | #[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
100 | pub enum ParseErrorLevel {
101 | /// Likely to be an mistake and should be noticed.
102 | ///
103 | /// The generator may generate code that contains this kind of mistakes.
104 | Note = 1,
105 | /// Should be a mistake but the compiler can guess a good way to generate proper code.
106 | Warn,
107 | /// An error that prevents a successful compilation, but can still continue to find more errors.
108 | Error,
109 | /// A very serious error that can cause continuous compiling issues, such as miss matched braces.
110 | Fatal,
111 | }
112 |
--------------------------------------------------------------------------------
/glass-easel/src/warning.ts:
--------------------------------------------------------------------------------
1 | import { type AnyComponent, type GeneralComponent } from './component'
2 | import { FuncArr, _setErrorDispatcher } from './func_arr'
3 | import { globalOptions } from './global_options'
4 | import { type Node, dumpSingleElementToString } from './node'
5 | import { isComponent } from './type_symbol'
6 |
7 | let insideErrorHandler = false
8 |
9 | const errorFuncArr = new FuncArr('error')
10 | const warningFuncArr = new FuncArr('warning')
11 |
12 | _setErrorDispatcher(dispatchError)
13 |
14 | export type ErrorListener = (
15 | error: unknown,
16 | method?: string,
17 | relatedComponent?: GeneralComponent | string,
18 | element?: Node,
19 | ) => boolean | void
20 |
21 | export type WarningListener = (
22 | message: string,
23 | relatedComponent: GeneralComponent | string,
24 | element?: Node,
25 | ) => boolean | void
26 |
27 | export function dispatchError(
28 | err: unknown,
29 | method?: string,
30 | relatedComponent?: AnyComponent | string,
31 | element?: Node,
32 | ) {
33 | if (!insideErrorHandler) {
34 | insideErrorHandler = true
35 | if (isComponent(relatedComponent)) {
36 | relatedComponent.triggerLifetime('error', [err])
37 | }
38 | const shouldBreak =
39 | errorFuncArr.call(err as any, [err, method, relatedComponent, element]) === false
40 | insideErrorHandler = false
41 | if (shouldBreak) return
42 | }
43 | if (globalOptions.throwGlobalError) {
44 | throw err
45 | } else {
46 | // eslint-disable-next-line no-console
47 | console.error(
48 | [
49 | err instanceof Error ? `${err.message}` : String(err),
50 | element ? `\t@${dumpSingleElementToString(element)}` : undefined,
51 | relatedComponent
52 | ? `\t${method || ''}@${
53 | typeof relatedComponent === 'string'
54 | ? `<${relatedComponent}>`
55 | : dumpSingleElementToString(relatedComponent || false)
56 | }`
57 | : undefined,
58 | ]
59 | .filter(Boolean)
60 | .join('\n'),
61 | )
62 | }
63 | }
64 |
65 | export function triggerWarning(
66 | msg: string,
67 | relatedComponent?: AnyComponent | string,
68 | element?: Node,
69 | ) {
70 | if (warningFuncArr.call(null, [msg, relatedComponent || '', element])) {
71 | // eslint-disable-next-line no-console
72 | console.warn(msg)
73 | }
74 | }
75 |
76 | export class ThirdError extends Error {
77 | constructor(
78 | message: string,
79 | public additionalStack?: string,
80 | public relatedComponent?: AnyComponent | string,
81 | public element?: Node,
82 | ) {
83 | super(message)
84 | dispatchError(this, additionalStack, relatedComponent, element)
85 | }
86 | }
87 |
88 | export function addGlobalErrorListener(func: ErrorListener) {
89 | errorFuncArr.add(func)
90 | }
91 |
92 | export function removeGlobalErrorListener(func: ErrorListener) {
93 | errorFuncArr.remove(func)
94 | }
95 |
96 | export function addGlobalWarningListener(func: WarningListener) {
97 | warningFuncArr.add(func)
98 | }
99 |
100 | export function removeGlobalWarningListener(func: WarningListener) {
101 | warningFuncArr.remove(func)
102 | }
103 |
104 | export function safeCallback any>(
105 | this: void,
106 | type: string,
107 | method: F,
108 | caller: ThisParameterType,
109 | args: Parameters,
110 | relatedComponent?: AnyComponent,
111 | ): ReturnType | undefined {
112 | try {
113 | // eslint-disable-next-line @typescript-eslint/no-unsafe-return
114 | return method.apply(caller, args)
115 | } catch (e) {
116 | dispatchError(e, `${type || 'Listener'} ${method.name || '(anonymous)'}`, relatedComponent)
117 | return undefined
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/glass-easel/src/dev_tools.ts:
--------------------------------------------------------------------------------
1 | import { type AnyComponent, type GeneralComponent } from './component'
2 | import type { Element } from './element'
3 | import { ENV, globalOptions } from './global_options'
4 |
5 | export { TmplDevArgs } from './tmpl/proc_gen_wrapper'
6 |
7 | export interface DevTools {
8 | inspector?: InspectorDevTools
9 | performance?: PerformanceDevTools
10 | }
11 |
12 | export interface MountPointEnv {}
13 |
14 | export interface InspectorDevTools {
15 | addMountPoint(root: Element, env: MountPointEnv): void
16 | removeMountPoint(root: Element): void
17 | }
18 |
19 | export interface PerformanceDevTools {
20 | now: () => number
21 | addTimelineEvent: (time: number, type: string, data?: Record) => void
22 | addTimelineComponentEvent: (
23 | time: number,
24 | type: string,
25 | component: GeneralComponent,
26 | data?: Record,
27 | ) => void
28 | addTimelinePerformanceMeasureStart: (
29 | time: number,
30 | type: string,
31 | component: GeneralComponent | string | null,
32 | data?: Record,
33 | ) => void
34 | addTimelinePerformanceMeasureEnd: (time: number) => void
35 | addTimelineBackendWaterfall: (
36 | type: string,
37 | times: [
38 | number, // pending time
39 | number, // start time
40 | number, // end time
41 | ],
42 | data?: Record,
43 | ) => void
44 | }
45 |
46 | let cachedDevTools: DevTools | undefined
47 |
48 | const getDevTools = (): DevTools | null | undefined => {
49 | if (!ENV.DEV) return null
50 | if (cachedDevTools === undefined) {
51 | cachedDevTools =
52 | globalOptions.devTools ||
53 | // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
54 | ((globalThis as any).__glassEaselDevTools__ as DevTools | undefined)
55 | }
56 | return cachedDevTools
57 | }
58 |
59 | export const performanceMeasureStart = (
60 | type: string,
61 | comp: AnyComponent | string | null = null,
62 | data?: Record,
63 | ) => {
64 | const perf = getDevTools()?.performance
65 | perf?.addTimelinePerformanceMeasureStart(perf.now(), type, comp, data)
66 | }
67 |
68 | export const performanceMeasureEnd = () => {
69 | const perf = getDevTools()?.performance
70 | perf?.addTimelinePerformanceMeasureEnd(perf.now())
71 | }
72 |
73 | export const performanceMeasureRenderWaterfall = (
74 | type: string,
75 | waterfallType: string,
76 | comp: AnyComponent,
77 | render: () => void,
78 | ) => {
79 | const perf = getDevTools()?.performance
80 | if (!perf) {
81 | render()
82 | return
83 | }
84 |
85 | const pendingTimestamp = perf.now()
86 | const nodeTreeContext = comp.getBackendContext()
87 | if (nodeTreeContext) {
88 | const measureId = nodeTreeContext.performanceTraceStart?.()
89 | performanceMeasureStart(type, comp)
90 | render()
91 | performanceMeasureEnd()
92 | nodeTreeContext.performanceTraceEnd?.(measureId!, ({ startTimestamp, endTimestamp }) => {
93 | perf.addTimelineBackendWaterfall(waterfallType, [
94 | pendingTimestamp,
95 | startTimestamp,
96 | endTimestamp,
97 | ])
98 | })
99 | } else {
100 | render()
101 | }
102 | }
103 |
104 | export const addTimelineEvent = (type: string, data?: Record) => {
105 | const perf = getDevTools()?.performance
106 | perf?.addTimelineEvent(perf.now(), type, data)
107 | }
108 |
109 | export const attachInspector = (elem: Element) => {
110 | if (!ENV.DEV) return
111 | const inspector = getDevTools()?.inspector
112 | inspector?.addMountPoint(elem, {})
113 | }
114 |
115 | export const detachInspector = (elem: Element) => {
116 | if (!ENV.DEV) return
117 | const inspector = getDevTools()?.inspector
118 | inspector?.removeMountPoint(elem)
119 | }
120 |
--------------------------------------------------------------------------------
/glass-easel-shadow-sync/src/utils.ts:
--------------------------------------------------------------------------------
1 | import type { Component, DataChange, Element, DataValue } from 'glass-easel'
2 |
3 | export interface IDGenerator {
4 | gen(): number
5 | release(id: number): void
6 | }
7 |
8 | export const getLinearIdGenerator = (): IDGenerator => {
9 | let nextId = 1
10 | const releasedIds: number[] = []
11 |
12 | const gen = (): number => {
13 | if (releasedIds.length) {
14 | return releasedIds.pop()!
15 | }
16 | const id = nextId
17 | nextId += 1
18 | return id
19 | }
20 |
21 | const release = (id: number) => {
22 | releasedIds.push(id)
23 | }
24 |
25 | return {
26 | gen,
27 | release,
28 | }
29 | }
30 |
31 | export const getRandomIdGenerator = (): IDGenerator => {
32 | let nextId = 1
33 | const releasedIds: number[] = []
34 |
35 | const gen = (): number => {
36 | const randomId = Math.floor(Math.random() * 16)
37 | if (randomId < releasedIds.length) {
38 | const targetId = releasedIds[randomId]!
39 | releasedIds.splice(randomId, 1)
40 | return targetId
41 | }
42 | const targetId = nextId + randomId - releasedIds.length
43 | while (nextId < targetId) {
44 | releasedIds.push(nextId)
45 | nextId += 1
46 | }
47 | nextId += 1
48 | return targetId
49 | }
50 |
51 | const release = (id: number) => {
52 | releasedIds.push(id)
53 | }
54 |
55 | return {
56 | gen,
57 | release,
58 | }
59 | }
60 |
61 | export function assertUnreachable(_x: never) {
62 | return new Error('unreachable')
63 | }
64 |
65 | export const dashToCamelCase = (dash: string): string =>
66 | dash.indexOf('-') <= 0 ? dash : dash.replace(/-(.|$)/g, (s) => (s[1] ? s[1].toUpperCase() : ''))
67 |
68 | export const camelCaseToDash = (camel: string): string =>
69 | camel.replace(/(^|.)([A-Z])/g, (s) => (s[0] ? `${s[0]}-` : '') + s[1]!.toLowerCase())
70 |
71 | export function initValues(element: Element, values: DataValue): void {
72 | const comp = element.asGeneralComponent() as Component
73 | if (!comp) return
74 |
75 | const val: Record = values || {}
76 | const data: Record = {}
77 | const keys = Object.keys(val)
78 | let externalClassDirty = false
79 | for (let i = 0; i < keys.length; i += 1) {
80 | const key = keys[i]!
81 | const dashName = camelCaseToDash(key)
82 | if (comp.hasExternalClass(dashName)) {
83 | comp.scheduleExternalClassChange(dashName, val[key] as string)
84 | externalClassDirty = true
85 | } else {
86 | data[key] = val[key]
87 | }
88 | }
89 | comp.setData(values as any)
90 | if (externalClassDirty) comp.applyExternalClassChanges()
91 | }
92 |
93 | export function updateValues(element: Element, changes: DataChange[]): void {
94 | const comp = element.asGeneralComponent() as Component
95 | if (!comp) return
96 |
97 | let dataDirty = false
98 | let externalClassDirty = false
99 |
100 | for (let i = 0; i < changes.length; i += 1) {
101 | const [path, newData, spliceIndex, spliceDel] = changes[i]!
102 | let dashName
103 | if (
104 | path.length === 1 &&
105 | typeof path[0] === 'string' &&
106 | // eslint-disable-next-line no-cond-assign
107 | ((dashName = camelCaseToDash(path[0])), comp.hasExternalClass(dashName))
108 | ) {
109 | comp.scheduleExternalClassChange(dashName, newData as string)
110 | externalClassDirty = true
111 | } else if (spliceDel !== undefined && spliceDel !== null) {
112 | comp.spliceArrayDataOnPath(path, spliceIndex, spliceDel, newData)
113 | dataDirty = true
114 | } else {
115 | comp.replaceDataOnPath(path, newData)
116 | dataDirty = true
117 | }
118 | }
119 | if (dataDirty) comp.applyDataUpdates()
120 | if (externalClassDirty) comp.applyExternalClassChanges()
121 | }
122 |
--------------------------------------------------------------------------------
/glass-easel/guide/zh_CN/data_management/advanced_update.md:
--------------------------------------------------------------------------------
1 | # 高级数据更新方法
2 |
3 | ## 使用 setData 更新模板数据绑定
4 |
5 | 一般情况下,可以使用组件实例的 `this.setData` 来更新数据绑定。
6 |
7 | ```js
8 | export const addComponent = componentSpace.defineComponent({
9 | template: compileTemplate(`
10 | {{ a }} + {{ b }} = {{ a + b }}
11 | `),
12 | data: {
13 | a: 1,
14 | b: 2,
15 | },
16 | lifetimes: {
17 | attached() {
18 | this.setData({
19 | a: 3,
20 | })
21 | },
22 | },
23 | })
24 | ```
25 |
26 | 在使用 Chaning API 时,也可以利用 init 函数这样做:
27 |
28 | ```js
29 | export const addComponent = componentSpace.define()
30 | .template(compileTemplate(`
31 | {{ a }} + {{ b }} = {{ a + b }}
32 | `))
33 | .data(() => ({
34 | a: 1,
35 | b: 2,
36 | }))
37 | .init(function ({ setData, lifetime }) {
38 | lifetime('attached', () => {
39 | // 这种做法可以省略掉 this
40 | setData({
41 | a: 3,
42 | })
43 | })
44 | })
45 | .registerComponent()
46 | ```
47 |
48 | setData 支持一些复杂的数据路径,例如:
49 |
50 | ```js
51 | export const addComponent = componentSpace.defineComponent({
52 | data: {
53 | obj: {
54 | a: [1, 2],
55 | },
56 | },
57 | lifetimes: {
58 | attached() {
59 | this.setData({
60 | 'obj.a[0]': 3,
61 | })
62 | },
63 | },
64 | })
65 | ```
66 |
67 | ## 高级路径更新
68 |
69 | 如果想仅更新对象内的一个字段,可以使用高级路径更新的方式。这样虽然接口复杂一些,但可以拥有更好的性能。
70 |
71 | 例如,可以使用 `replaceDataOnPath` 来更新对象内的数据字段:
72 |
73 | ```js
74 | export const addComponent = componentSpace.defineComponent({
75 | data: {
76 | obj: {
77 | a: [1, 2],
78 | },
79 | },
80 | lifetimes: {
81 | attached() {
82 | // 更新 obj.a 字段
83 | this.replaceDataOnPath(['obj', 'a', 0], 3)
84 | // 将更新应用到模板上
85 | this.applyDataUpdates()
86 | },
87 | },
88 | })
89 | ```
90 |
91 | 如果字段是数组类型的,还可以使用 `spliceArrayDataOnPath` 来对数组项进行插入和删除:
92 |
93 | ```js
94 | export const addComponent = componentSpace.defineComponent({
95 | data: {
96 | obj: {
97 | arr: [1, 2, 3, 4],
98 | },
99 | },
100 | lifetimes: {
101 | attached() {
102 | // 更新 obj.arr 数组:
103 | // 类似于数组的 splice 方法
104 | // 可以在一个位置上移除若干项,再插入若干项
105 | this.spliceArrayDataOnPath(['obj', 'arr'], 1, 2, [5, 6, 7])
106 | // 得到的 obj.arr 是 [1, 5, 6, 7, 4]
107 | // 将更新应用到模板上
108 | this.applyDataUpdates()
109 | },
110 | },
111 | })
112 | ```
113 |
114 | 调用 `replaceDataOnPath` 和 `spliceArrayDataOnPath` 后,并不会立即将更新内容应用到模板上,还需要调用 `applyDataUpdates` 。如果需要连续调用多个 `replaceDataOnPath` 或 `spliceArrayDataOnPath` ,在末尾调用一次 `applyDataUpdates` 即可。
115 |
116 | ## 组合更新
117 |
118 | 连续调用多个 `replaceDataOnPath` 或 `spliceArrayDataOnPath` 后,可能会遗忘调用 `applyDataUpdates` 。
119 |
120 | 可以考虑改用 `groupUpdates` 将它们组合起来,例如:
121 |
122 | ```js
123 | export const addComponent = componentSpace.defineComponent({
124 | data: {
125 | obj: {
126 | a: 1,
127 | b: 2,
128 | },
129 | },
130 | lifetimes: {
131 | attached() {
132 | // 进行组合更新
133 | this.groupUpdates(() => {
134 | this.replaceDataOnPath(['obj', 'a'], 3)
135 | this.replaceDataOnPath(['obj', 'b'], 5)
136 | })
137 | },
138 | },
139 | })
140 | ```
141 |
142 | 在 `groupUpdates` 调用后,会自动将更新应用到模板上,不再需要调用 `applyDataUpdates` 。
143 |
144 | 另外,如果需要将多个 `setData` 调用组合起来,可以使用 `updateData` ,例如:
145 |
146 | ```js
147 | export const addComponent = componentSpace.defineComponent({
148 | template: compileTemplate(`
149 | {{ a }} + {{ b }} = {{ a + b }}
150 | `),
151 | data: {
152 | a: 1,
153 | b: 2,
154 | },
155 | lifetimes: {
156 | attached() {
157 | this.groupUpdates(() => {
158 | this.updateData({
159 | a: 3,
160 | })
161 | this.updateData({
162 | b: 5,
163 | })
164 | })
165 | },
166 | },
167 | })
168 | ```
169 |
170 | 这种做法性能上优于连续多次 `setData` 。
171 |
--------------------------------------------------------------------------------